feat: iCloud KV Store sync for tasks, workout data, and settings

- Add NSUbiquitousKeyValueStore entitlement to KisaniCal.entitlements
- New CloudSyncManager: mirrors UserDefaults to iCloud KV Store on every save; restores to UserDefaults on fresh install when local data is missing; observes external changes to pull updates in real-time; guards against 1 MB limit
- TaskViewModel: restore from iCloud on init, push to iCloud on every save
- WorkoutViewModel: restore programs/schedule/activePid/completedDates from iCloud on init, push all four keys on every save
- ContentView: restoreSettings() + backupSettings() on appear and foreground — syncs ~18 small settings keys (appearance, tabs, tutorial flags, calendar prefs, workout notifications)

Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
This commit is contained in:
Robin Kutesa
2026-06-01 20:45:26 +03:00
parent 070dd77b4e
commit 0647583036
7 changed files with 225 additions and 21 deletions

View File

@@ -216,6 +216,12 @@ class WorkoutViewModel: ObservableObject {
self.kActivePid = "kisani.workout.activePid.\(uid)"
self.kWorkoutDates = "kisani.workout.completedDates.\(uid)"
// 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.schedule.\(uid)")
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.activePid.\(uid)")
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.completedDates.\(uid)")
// Attempt to restore persisted state
if let data = UserDefaults.standard.data(forKey: kPrograms),
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
@@ -283,11 +289,15 @@ class WorkoutViewModel: ObservableObject {
private func save() {
if let data = try? JSONEncoder().encode(programs) {
UserDefaults.standard.set(data, forKey: kPrograms)
CloudSyncManager.shared.push(data: data, forKey: kPrograms)
}
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
UserDefaults.standard.set(schedDict, forKey: kSchedule)
UserDefaults.standard.set(activeProgramId.uuidString, forKey: kActivePid)
UserDefaults.standard.set(workoutDates, forKey: kWorkoutDates)
CloudSyncManager.shared.push(value: schedDict, forKey: kSchedule)
CloudSyncManager.shared.push(value: activeProgramId.uuidString, forKey: kActivePid)
CloudSyncManager.shared.push(value: workoutDates, forKey: kWorkoutDates)
}
func logWorkoutCompleted(on date: Date = Date()) {

View File

@@ -129,6 +129,7 @@ class TaskViewModel: ObservableObject {
init() {
let uid = AuthManager.shared.currentUser?.id ?? "shared"
self.persistKey = "kisani.tasks.v2.\(uid)"
CloudSyncManager.shared.restoreIfMissing(key: persistKey)
if let data = UserDefaults.standard.data(forKey: persistKey),
let saved = try? JSONDecoder().decode([TaskItem].self, from: data), !saved.isEmpty {
tasks = saved
@@ -147,6 +148,7 @@ class TaskViewModel: ObservableObject {
private func save() {
if let data = try? JSONEncoder().encode(tasks) {
UserDefaults.standard.set(data, forKey: persistKey)
CloudSyncManager.shared.push(data: data, forKey: persistKey)
}
}