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 {
|
.onAppear {
|
||||||
if !hasOnboarded { showOnboarding = true }
|
if !hasOnboarded { showOnboarding = true }
|
||||||
|
workoutVM.resetSetsForNewDayIfNeeded()
|
||||||
CloudSyncManager.shared.restoreSettings()
|
CloudSyncManager.shared.restoreSettings()
|
||||||
CloudSyncManager.shared.backupSettings()
|
CloudSyncManager.shared.backupSettings()
|
||||||
HealthKitManager.shared.bootstrap()
|
HealthKitManager.shared.bootstrap()
|
||||||
@@ -108,6 +109,7 @@ struct ContentView: View {
|
|||||||
if phase == .active {
|
if phase == .active {
|
||||||
taskVM.reload()
|
taskVM.reload()
|
||||||
consumeWidgetAddTask()
|
consumeWidgetAddTask()
|
||||||
|
workoutVM.resetSetsForNewDayIfNeeded()
|
||||||
workoutVM.checkPendingHealthConfirm()
|
workoutVM.checkPendingHealthConfirm()
|
||||||
CloudSyncManager.shared.backupSettings()
|
CloudSyncManager.shared.backupSettings()
|
||||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
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 kSchedule: String
|
||||||
private let kActivePid: String
|
private let kActivePid: String
|
||||||
private let kWorkoutDates: String
|
private let kWorkoutDates: String
|
||||||
|
private let kLastActiveDay: String
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
||||||
@@ -215,6 +216,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
self.kSchedule = "kisani.workout.schedule.\(uid)"
|
self.kSchedule = "kisani.workout.schedule.\(uid)"
|
||||||
self.kActivePid = "kisani.workout.activePid.\(uid)"
|
self.kActivePid = "kisani.workout.activePid.\(uid)"
|
||||||
self.kWorkoutDates = "kisani.workout.completedDates.\(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)
|
// Restore all workout keys from iCloud if missing locally (fresh install / new build)
|
||||||
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.programs.\(uid)")
|
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.programs.\(uid)")
|
||||||
@@ -284,6 +286,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
activeSections = blank.sections
|
activeSections = blank.sections
|
||||||
}
|
}
|
||||||
workoutDates = UserDefaults.kisani.stringArray(forKey: kWorkoutDates) ?? []
|
workoutDates = UserDefaults.kisani.stringArray(forKey: kWorkoutDates) ?? []
|
||||||
|
resetSetsForNewDayIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
@@ -300,6 +303,36 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
CloudSyncManager.shared.push(value: workoutDates, forKey: kWorkoutDates)
|
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()) {
|
func logWorkoutCompleted(on date: Date = Date()) {
|
||||||
let key = iso.string(from: date)
|
let key = iso.string(from: date)
|
||||||
guard !workoutDates.contains(key) else { return }
|
guard !workoutDates.contains(key) else { return }
|
||||||
@@ -452,6 +485,8 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
else { return }
|
else { return }
|
||||||
let wasMarkedDone = !activeSections[si].exercises[ei].sets[setIdx].isDone
|
let wasMarkedDone = !activeSections[si].exercises[ei].sets[setIdx].isDone
|
||||||
activeSections[si].exercises[ei].sets[setIdx].isDone.toggle()
|
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 {
|
if sessionStartDate == nil, activeSections[si].exercises[ei].sets[setIdx].isDone {
|
||||||
sessionStartDate = Date()
|
sessionStartDate = Date()
|
||||||
}
|
}
|
||||||
|
|||||||