fix: reset workout log each new day + new app logo
KC-1: Set completion was stored permanently on the program, so the same program scheduled on back-to-back days (e.g. Shoulders Mon + Tue) kept yesterday's checkmarks. Add resetSetsForNewDayIfNeeded() to clear every set's isDone when the calendar day changes (cold launch, onAppear, and scenePhase active). Streak history is untouched. KC-2: Replace all 12 AppIcon sizes with the new KisaniCal mark, alpha flattened onto RGB(26,29,34) so the 1024 App Store icon is fully opaque. Add KisaniCal/ISSUES.md tracker (KC-1, KC-2: In Progress). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -93,6 +93,7 @@ struct ContentView: View {
|
||||
}
|
||||
.onAppear {
|
||||
if !hasOnboarded { showOnboarding = true }
|
||||
workoutVM.resetSetsForNewDayIfNeeded()
|
||||
CloudSyncManager.shared.restoreSettings()
|
||||
CloudSyncManager.shared.backupSettings()
|
||||
HealthKitManager.shared.bootstrap()
|
||||
@@ -108,6 +109,7 @@ struct ContentView: View {
|
||||
if phase == .active {
|
||||
taskVM.reload()
|
||||
consumeWidgetAddTask()
|
||||
workoutVM.resetSetsForNewDayIfNeeded()
|
||||
workoutVM.checkPendingHealthConfirm()
|
||||
CloudSyncManager.shared.backupSettings()
|
||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||
|
||||
58
KisaniCal/ISSUES.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# KisaniCal — Issue Tracker
|
||||
|
||||
Status legend: `Open` · `In Progress` · `Resolved`
|
||||
|
||||
---
|
||||
|
||||
## KC-1 — Daily workout log does not reset on a new day
|
||||
|
||||
**Status:** In Progress (fix implemented & tested locally, pending release)
|
||||
**Reported by:** User
|
||||
**Area:** Workout
|
||||
|
||||
### Description
|
||||
When the same workout program is scheduled on back-to-back days (e.g. Shoulders
|
||||
on Monday **and** Tuesday), the new day does not start fresh. Completed sets from
|
||||
the previous day stay checked, so the workout appears already done.
|
||||
|
||||
### Root cause
|
||||
Set completion (`ExerciseSet.isDone`) was stored permanently inside the
|
||||
`WorkoutProgram` and persisted via `save()`. There was no per-day reset, so a
|
||||
program reused the next day carried over yesterday's checkmarks.
|
||||
|
||||
### Fix
|
||||
Set completion is now a per-day state:
|
||||
- `WorkoutViewModel.resetSetsForNewDayIfNeeded()` clears every set's `isDone`
|
||||
across all programs when the calendar day changes (skips first launch, no-ops
|
||||
when the day is unchanged).
|
||||
- `toggleSet(...)` stamps the active day so on-screen state always belongs to today.
|
||||
- Called on cold launch (`init`), `.onAppear`, and `scenePhase == .active`
|
||||
(handles crossing midnight while the app is alive).
|
||||
- Streak history (`workoutDates`) is untouched, so completed days still count.
|
||||
|
||||
Files: `KisaniCal/Models/ExerciseModels.swift`, `KisaniCal/ContentView.swift`
|
||||
|
||||
### Verification
|
||||
Builds cleanly (Xcode 26.2 SDK, simulator). Same program on consecutive days now
|
||||
starts unchecked each day; streak preserved.
|
||||
|
||||
---
|
||||
|
||||
## KC-2 — App logo update (new KisaniCal brand mark)
|
||||
|
||||
**Status:** In Progress (assets installed & build-verified, pending release)
|
||||
**Area:** Branding / App Icon
|
||||
|
||||
### Description
|
||||
Replace the app icon with the new KisaniCal logo (orange dot grid on dark) across
|
||||
the App Store marketing icon and all home/lock screen / Spotlight / notification sizes.
|
||||
|
||||
### Fix
|
||||
All 12 sizes in `AppIcon.appiconset` regenerated from the new source. Alpha
|
||||
flattened onto the brand dark background `RGB(26,29,34)` so the 1024 App Store icon
|
||||
is fully opaque (App Store Connect compliant) and device icons are full-bleed.
|
||||
|
||||
Files: `KisaniCal/Assets.xcassets/AppIcon.appiconset/`
|
||||
|
||||
### Verification
|
||||
Builds cleanly (app + widget extension); `hasAlpha: no` on all icons.
|
||||
@@ -208,6 +208,7 @@ class WorkoutViewModel: ObservableObject {
|
||||
private let kSchedule: String
|
||||
private let kActivePid: String
|
||||
private let kWorkoutDates: String
|
||||
private let kLastActiveDay: String
|
||||
|
||||
init() {
|
||||
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
||||
@@ -215,6 +216,7 @@ class WorkoutViewModel: ObservableObject {
|
||||
self.kSchedule = "kisani.workout.schedule.\(uid)"
|
||||
self.kActivePid = "kisani.workout.activePid.\(uid)"
|
||||
self.kWorkoutDates = "kisani.workout.completedDates.\(uid)"
|
||||
self.kLastActiveDay = "kisani.workout.lastActiveDay.\(uid)"
|
||||
|
||||
// Restore all workout keys from iCloud if missing locally (fresh install / new build)
|
||||
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.programs.\(uid)")
|
||||
@@ -284,6 +286,7 @@ class WorkoutViewModel: ObservableObject {
|
||||
activeSections = blank.sections
|
||||
}
|
||||
workoutDates = UserDefaults.kisani.stringArray(forKey: kWorkoutDates) ?? []
|
||||
resetSetsForNewDayIfNeeded()
|
||||
}
|
||||
|
||||
private func save() {
|
||||
@@ -300,6 +303,36 @@ class WorkoutViewModel: ObservableObject {
|
||||
CloudSyncManager.shared.push(value: workoutDates, forKey: kWorkoutDates)
|
||||
}
|
||||
|
||||
// MARK: - Daily Reset
|
||||
/// A fresh calendar day starts a clean log. If the checkbox state currently on
|
||||
/// screen belongs to an earlier day, wipe every set's `isDone` so re-running the
|
||||
/// same program on back-to-back days (e.g. Shoulders on Mon AND Tue) starts
|
||||
/// unchecked. Streak history (`workoutDates`) is never touched here.
|
||||
/// Safe to call repeatedly — it only does work when the day actually changes.
|
||||
func resetSetsForNewDayIfNeeded() {
|
||||
let today = iso.string(from: Date())
|
||||
let last = UserDefaults.kisani.string(forKey: kLastActiveDay)
|
||||
guard last != today else { return }
|
||||
if last != nil { clearAllDoneSets() } // skip the wipe on the very first launch
|
||||
UserDefaults.kisani.set(today, forKey: kLastActiveDay)
|
||||
}
|
||||
|
||||
/// Unchecks every set across all programs and clears the in-progress session.
|
||||
private func clearAllDoneSets() {
|
||||
for pi in programs.indices {
|
||||
for si in programs[pi].sections.indices {
|
||||
for ei in programs[pi].sections[si].exercises.indices {
|
||||
for setIdx in programs[pi].sections[si].exercises[ei].sets.indices {
|
||||
programs[pi].sections[si].exercises[ei].sets[setIdx].isDone = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
activeSections = programs.first { $0.id == activeProgramId }?.sections ?? activeSections
|
||||
sessionStartDate = nil
|
||||
save()
|
||||
}
|
||||
|
||||
func logWorkoutCompleted(on date: Date = Date()) {
|
||||
let key = iso.string(from: date)
|
||||
guard !workoutDates.contains(key) else { return }
|
||||
@@ -452,6 +485,8 @@ class WorkoutViewModel: ObservableObject {
|
||||
else { return }
|
||||
let wasMarkedDone = !activeSections[si].exercises[ei].sets[setIdx].isDone
|
||||
activeSections[si].exercises[ei].sets[setIdx].isDone.toggle()
|
||||
// The current checkbox state now belongs to today.
|
||||
UserDefaults.kisani.set(iso.string(from: Date()), forKey: kLastActiveDay)
|
||||
if sessionStartDate == nil, activeSections[si].exercises[ei].sets[setIdx].isDone {
|
||||
sessionStartDate = Date()
|
||||
}
|
||||
|
||||