feat: HealthKit workout sync, confirmation notification, health stats on Today dashboard

- HealthKitManager: add fetchWorkoutDates(from:to:) to read past workouts from Apple Health
- WorkoutViewModel: syncFromHealthKit() merges HealthKit workout dates into streak (runs on every foreground); checkPendingHealthConfirm() picks up confirmed workouts from notification action
- NotificationManager: WORKOUT_CONFIRM category with 'Yes, log it' action; scheduleWorkoutConfirm() fires weekly on scheduled workout days at configurable time (default 8pm); LOG_WORKOUT action writes pendingConfirm to UserDefaults
- SettingsView (Workout Settings): new HEALTH SYNC section with 'Sync now' button and 'Workout confirmation reminder' toggle + time picker
- TodayView: TodayHealthStrip shows steps, active calories, resting HR, and workout streak in a 4-pill row below the date header (only shown when HealthKit is authorized); refreshes on appear and foreground
- FAB: Matrix FAB padding fixed to match Today/Calendar position (.bottom 10)

Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
This commit is contained in:
Robin Kutesa
2026-06-02 13:10:32 +03:00
parent 0647583036
commit e5bd739499
9 changed files with 385 additions and 48 deletions

View File

@@ -310,6 +310,35 @@ class WorkoutViewModel: ObservableObject {
sessionStartDate = nil
}
// Pull completed workout dates from Apple Health and merge into local streak
@MainActor
func syncFromHealthKit() async {
guard HealthKitManager.shared.authorized else { return }
guard let ninetyDaysAgo = Calendar.current.date(byAdding: .day, value: -90, to: Date()) else { return }
let hkDates = await HealthKitManager.shared.fetchWorkoutDates(from: ninetyDaysAgo, to: Date())
guard !hkDates.isEmpty else { return }
var newSet = Set(workoutDates); var updated = false
for d in hkDates where !newSet.contains(d) { newSet.insert(d); updated = true }
if updated { workoutDates = Array(newSet); save() }
}
// Called on foreground picks up "Yes I did it" tapped in notification
@MainActor
func checkPendingHealthConfirm() {
let key = "kisani.workout.pendingConfirm"
guard let dateStr = UserDefaults.standard.string(forKey: key) else { return }
UserDefaults.standard.removeObject(forKey: key)
guard !workoutDates.contains(dateStr) else { return }
workoutDates.append(dateStr)
save()
let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
if let date = fmt.date(from: dateStr) {
let s = Calendar.current.startOfDay(for: date)
let e = Calendar.current.date(byAdding: .hour, value: 1, to: s) ?? s
Task { await HealthKitManager.shared.saveWorkout(start: s, end: e) }
}
}
func seedStreak(days: Int) {
let cal = Calendar.current
var newDates = Set(workoutDates)