Workout streak: lifetime tally that only grows (never resets)
Per user model: the workout count should reflect every workout achieved and never break. Replaces the consecutive/weekly-goal streak with a simple count of distinct workout days (totalAchievedWorkoutDays). A below-goal week (4/5) and a full week (5/5) both count fully (=9); a missed day or blank week never reduces it; duplicate HealthKit days count once. - ExerciseModels: streakDays = Set(workoutDates).count - StreakLogicTests: retargeted to tally semantics (4/5+5/5=9, blank-week, duplicates, skipped-day, empty) - SettingsView: goal caption no longer claims the streak can be lost NOTE: not built/tested locally — CLI xcodebuild wedges on actool via the broken CoreSimulator daemon (missing sim runtimes, machine-wide). Verify in Xcode GUI (prefs now fixed) or on device. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -249,50 +249,17 @@ class WorkoutViewModel: ObservableObject {
|
||||
@Published var restTimerTotal: Int = 0
|
||||
@Published var sessionStartDate: Date? = nil
|
||||
|
||||
/// Weekly-goal streak, per the Settings promise: "Reach your goal every week
|
||||
/// to maintain your streak." Counts WORKOUT DAYS across consecutive kept weeks,
|
||||
/// so rest days never break it and one skipped day within the goal doesn't
|
||||
/// reset it. The current (in-progress) week never breaks the chain.
|
||||
/// Lifetime tally of workout days achieved. Per the user's model this only ever
|
||||
/// grows — a missed day, an off week, even a fully-blank week never reduce it.
|
||||
/// A 4/5 week still contributes its 4; there is no "break." Not a consecutive
|
||||
/// streak: it's the sum of everything you've actually done.
|
||||
var streakDays: Int {
|
||||
let goal = UserDefaults.standard.object(forKey: "streakGoal") as? Int ?? 5
|
||||
return Self.weeklyGoalStreak(workoutDates: Set(workoutDates),
|
||||
scheduledDayCount: schedule.count,
|
||||
goal: goal,
|
||||
today: Date(),
|
||||
calendar: Calendar.current)
|
||||
Self.totalAchievedWorkoutDays(workoutDates)
|
||||
}
|
||||
|
||||
/// Pure streak math (unit-tested in StreakLogicTests).
|
||||
/// - A past week is "kept" when its workout days ≥ min(goal, scheduled days).
|
||||
/// - Streak = total workout days over the current week plus consecutive kept weeks.
|
||||
static func weeklyGoalStreak(workoutDates: Set<String>,
|
||||
scheduledDayCount: Int,
|
||||
goal: Int,
|
||||
today: Date,
|
||||
calendar cal: Calendar) -> Int {
|
||||
let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
|
||||
let target = scheduledDayCount > 0 ? min(max(goal, 1), scheduledDayCount) : max(goal, 1)
|
||||
|
||||
func daysDone(inWeekOf ref: Date) -> Int {
|
||||
guard let week = cal.dateInterval(of: .weekOfYear, for: ref) else { return 0 }
|
||||
var n = 0, d = week.start
|
||||
while d < week.end {
|
||||
if workoutDates.contains(fmt.string(from: d)) { n += 1 }
|
||||
d = cal.date(byAdding: .day, value: 1, to: d)!
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
var streak = daysDone(inWeekOf: today) // current week: always counts, never breaks
|
||||
var ref = today
|
||||
for _ in 0..<520 { // walk back up to 10 years
|
||||
guard let prev = cal.date(byAdding: .weekOfYear, value: -1, to: ref) else { break }
|
||||
let done = daysDone(inWeekOf: prev)
|
||||
guard done >= target else { break }
|
||||
streak += done
|
||||
ref = prev
|
||||
}
|
||||
return streak
|
||||
/// Count of distinct workout days ever logged (unit-tested in StreakLogicTests).
|
||||
static func totalAchievedWorkoutDays(_ workoutDates: [String]) -> Int {
|
||||
Set(workoutDates).count
|
||||
}
|
||||
|
||||
private let iso: DateFormatter = {
|
||||
|
||||
Reference in New Issue
Block a user