Clear demo data, namespace storage by user, add profile setup

- Remove all personal sampleTasks from TaskViewModel (tasks start empty)
- Namespace UserDefaults keys by Apple ID for task and workout data isolation
- Add ProfileSetupView for name capture after first Sign in with Apple
- Add updateDisplayName() to AuthManager
- Route to ProfileSetupView when user has no display name yet

Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
This commit is contained in:
Robin Kutesa
2026-06-01 01:31:55 +03:00
parent ac7f2d0614
commit d424a2a814
6 changed files with 213 additions and 79 deletions

View File

@@ -175,22 +175,66 @@ class WorkoutViewModel: ObservableObject {
@Published var schedule: [Int: UUID]
@Published var activeProgramId: UUID
@Published var activeSections: [WorkoutSection]
@Published var streakDays: Int = 5
@Published var workoutDates: [String] = []
@Published var restTimerSeconds: Int = 60
@Published var sessionStartDate: Date? = nil
var streakDays: Int {
let cal = Calendar.current
let fmt = iso; let dateSet = Set(workoutDates)
var streak = 0
var check = cal.startOfDay(for: Date())
if !dateSet.contains(fmt.string(from: check)) {
check = cal.date(byAdding: .day, value: -1, to: check)!
}
while dateSet.contains(fmt.string(from: check)) {
streak += 1
check = cal.date(byAdding: .day, value: -1, to: check)!
}
return streak
}
private let iso: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd"; return f
}()
@Published var showAddExercise: Bool = false
@Published var addExerciseTargetSectionId: UUID?
private static let kPrograms = "kisani.workout.programs"
private static let kSchedule = "kisani.workout.schedule"
private static let kActivePid = "kisani.workout.activePid"
private let kPrograms: String
private let kSchedule: String
private let kActivePid: String
private let kWorkoutDates: String
init() {
let uid = AuthManager.shared.currentUser?.id ?? "shared"
self.kPrograms = "kisani.workout.programs.\(uid)"
self.kSchedule = "kisani.workout.schedule.\(uid)"
self.kActivePid = "kisani.workout.activePid.\(uid)"
self.kWorkoutDates = "kisani.workout.completedDates.\(uid)"
// Attempt to restore persisted state
if let data = UserDefaults.standard.data(forKey: Self.kPrograms),
if let data = UserDefaults.standard.data(forKey: kPrograms),
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
!saved.isEmpty {
let emojiMap = ["🔥": "🔄", "💥": "🏋️"]
// Migrate any legacy emoji SF Symbol name
let emojiMap: [String: String] = [
"🏋️": "figure.strengthtraining.traditional",
"💪": "figure.strengthtraining.functional",
"🔄": "figure.arms.open",
"🦵": "figure.run",
"": "bolt.fill",
"⚡️": "bolt.fill",
"🏃": "figure.run",
"🤸": "figure.yoga",
"🧘": "figure.yoga",
"🚴": "figure.cycling",
"🥊": "figure.boxing",
"🎯": "figure.core.training",
"🫀": "heart.fill",
"🔥": "bolt.fill",
"💥": "figure.highintensity.intervaltraining"
]
let needsMigration = saved.contains(where: { emojiMap[$0.emoji] != nil })
let migrated: [WorkoutProgram] = needsMigration
? saved.map { p in
@@ -200,7 +244,7 @@ class WorkoutViewModel: ObservableObject {
: saved
var sched: [Int: UUID] = [:]
if let dict = UserDefaults.standard.dictionary(forKey: Self.kSchedule) as? [String: String] {
if let dict = UserDefaults.standard.dictionary(forKey: kSchedule) as? [String: String] {
for (k, v) in dict {
if let day = Int(k), let uid = UUID(uuidString: v) { sched[day] = uid }
}
@@ -208,7 +252,7 @@ class WorkoutViewModel: ObservableObject {
schedule = sched
var pid = saved[0].id
if let str = UserDefaults.standard.string(forKey: Self.kActivePid),
if let str = UserDefaults.standard.string(forKey: kActivePid),
let uid = UUID(uuidString: str),
saved.contains(where: { $0.id == uid }) { pid = uid }
activeProgramId = pid
@@ -219,30 +263,17 @@ class WorkoutViewModel: ObservableObject {
if needsMigration { save() }
} else {
// First launch: seed with sample programs
let progs = WorkoutViewModel.samplePrograms()
programs = progs
var sched: [Int: UUID] = [:]
if progs.count >= 6 {
sched[2] = progs[0].id
sched[3] = progs[1].id
sched[4] = progs[2].id
sched[5] = progs[3].id
sched[6] = progs[4].id
sched[7] = progs[5].id
}
schedule = sched
let today = Calendar.current.component(.weekday, from: Date())
if let pid = sched[today], progs.contains(where: { $0.id == pid }) {
activeProgramId = pid
activeSections = progs.first { $0.id == pid }!.sections
} else {
activeProgramId = progs[0].id
activeSections = progs[0].sections
}
// First launch: start blank so the user builds their own routine
let blank = WorkoutProgram(
name: "My Workout", emoji: "dumbbell.fill", colorIndex: 0,
sections: [WorkoutSection(name: "Main", exercises: [])]
)
programs = [blank]
schedule = [:]
activeProgramId = blank.id
activeSections = blank.sections
}
workoutDates = UserDefaults.standard.stringArray(forKey: kWorkoutDates) ?? []
}
private func save() {
@@ -250,13 +281,41 @@ class WorkoutViewModel: ObservableObject {
UserDefaults.standard.set(data, forKey: Self.kPrograms)
}
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
UserDefaults.standard.set(schedDict, forKey: Self.kSchedule)
UserDefaults.standard.set(schedDict, forKey: Self.kSchedule)
UserDefaults.standard.set(activeProgramId.uuidString, forKey: Self.kActivePid)
UserDefaults.standard.set(workoutDates, forKey: Self.kWorkoutDates)
}
func logWorkoutCompleted(on date: Date = Date()) {
let key = iso.string(from: date)
guard !workoutDates.contains(key) else { return }
workoutDates.append(key)
save()
let start = sessionStartDate ?? date.addingTimeInterval(-3600)
Task { await HealthKitManager.shared.saveWorkout(start: start, end: date) }
sessionStartDate = nil
}
func seedStreak(days: Int) {
let cal = Calendar.current
var newDates = Set(workoutDates)
for i in 0..<days {
let d = cal.date(byAdding: .day, value: -i, to: cal.startOfDay(for: Date()))!
newDates.insert(iso.string(from: d))
}
workoutDates = Array(newDates)
save()
}
func setScheduleForDays(_ weekdays: Set<Int>) {
schedule = [:]
for day in weekdays { schedule[day] = activeProgramId }
save()
}
var activeProgram: WorkoutProgram? { programs.first { $0.id == activeProgramId } }
var workoutTitle: String { activeProgram?.name ?? "Workout" }
var workoutEmoji: String { activeProgram?.emoji ?? "💪" }
var workoutEmoji: String { activeProgram?.emoji ?? "dumbbell.fill" }
var totalSets: Int { activeSections.reduce(0) { $0 + $1.totalSets } }
var doneSets: Int { activeSections.reduce(0) { $0 + $1.doneSets } }
var progress: Double { totalSets > 0 ? Double(doneSets) / Double(totalSets) : 0 }
@@ -345,7 +404,11 @@ class WorkoutViewModel: ObservableObject {
let setIdx = activeSections[si].exercises[ei].sets.firstIndex(where: { $0.id == setId })
else { return }
activeSections[si].exercises[ei].sets[setIdx].isDone.toggle()
if sessionStartDate == nil, activeSections[si].exercises[ei].sets[setIdx].isDone {
sessionStartDate = Date()
}
syncBack()
if doneSets == totalSets && totalSets > 0 { logWorkoutCompleted() }
}
func addSet(sectionId: UUID, to exerciseId: UUID) {
@@ -453,7 +516,7 @@ class WorkoutViewModel: ObservableObject {
let lb: Double = 0.453592
// Monday: Chest & Triceps
let mon = WorkoutProgram(name: "Chest & Triceps", emoji: "🏋️", colorIndex: 0, sections: [
let mon = WorkoutProgram(name: "Chest & Triceps", emoji: "figure.strengthtraining.traditional", colorIndex: 0, sections: [
warmUp,
WorkoutSection(name: "Chest", exercises: [
// Incline first 70 / 65 / 45 / 35 lbs × 12 reps (per-side dumbbell)
@@ -499,7 +562,7 @@ class WorkoutViewModel: ObservableObject {
])
// Tuesday: Back & Biceps
let tue = WorkoutProgram(name: "Back & Biceps", emoji: "💪", colorIndex: 1, sections: [
let tue = WorkoutProgram(name: "Back & Biceps", emoji: "figure.strengthtraining.functional", colorIndex: 1, sections: [
warmUp,
WorkoutSection(name: "Back", exercises: [
Exercise(name: "Lat Pulldown", sfSymbol: "figure.pull.up", colorIndex: 1, sets: [
@@ -538,7 +601,7 @@ class WorkoutViewModel: ObservableObject {
])
// Wednesday: Shoulders, Triceps & Biceps
let wed = WorkoutProgram(name: "Shoulders & Arms", emoji: "🔄", colorIndex: 2, sections: [
let wed = WorkoutProgram(name: "Shoulders & Arms", emoji: "figure.arms.open", colorIndex: 2, sections: [
warmUp,
WorkoutSection(name: "Shoulders", exercises: [
Exercise(name: "Reverse Fly", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
@@ -557,7 +620,7 @@ class WorkoutViewModel: ObservableObject {
])
// Thursday: Legs & Chest
let thu = WorkoutProgram(name: "Legs & Chest", emoji: "🦵", colorIndex: 3, sections: [
let thu = WorkoutProgram(name: "Legs & Chest", emoji: "figure.run", colorIndex: 3, sections: [
warmUp,
WorkoutSection(name: "Legs", exercises: [
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 2, sets: s(3)),
@@ -574,7 +637,7 @@ class WorkoutViewModel: ObservableObject {
])
// Friday: Full Body
let fri = WorkoutProgram(name: "Full Body", emoji: "", colorIndex: 0, sections: [
let fri = WorkoutProgram(name: "Full Body", emoji: "bolt.fill", colorIndex: 0, sections: [
warmUp,
WorkoutSection(name: "Upper Body", exercises: [
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
@@ -591,7 +654,7 @@ class WorkoutViewModel: ObservableObject {
])
// Saturday: Full Body
let sat = WorkoutProgram(name: "Full Body", emoji: "🏋️", colorIndex: 1, sections: [
let sat = WorkoutProgram(name: "Full Body", emoji: "figure.highintensity.intervaltraining", colorIndex: 1, sections: [
warmUp,
WorkoutSection(name: "Upper Body", exercises: [
Exercise(name: "Incline Bench Press",sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),

View File

@@ -58,20 +58,22 @@ enum TaskColor: String, Codable {
class TaskViewModel: ObservableObject {
@Published var tasks: [TaskItem]
private static let persistKey = "kisani.tasks.v2"
private let persistKey: String
init() {
if let data = UserDefaults.standard.data(forKey: Self.persistKey),
let uid = AuthManager.shared.currentUser?.id ?? "shared"
self.persistKey = "kisani.tasks.v2.\(uid)"
if let data = UserDefaults.standard.data(forKey: persistKey),
let saved = try? JSONDecoder().decode([TaskItem].self, from: data), !saved.isEmpty {
tasks = saved
} else {
tasks = sampleTasks
tasks = []
}
}
private func save() {
if let data = try? JSONEncoder().encode(tasks) {
UserDefaults.standard.set(data, forKey: Self.persistKey)
UserDefaults.standard.set(data, forKey: persistKey)
}
}
@@ -183,37 +185,3 @@ class TaskViewModel: ObservableObject {
}
}
// MARK: - Sample Data
private let sampleTasks: [TaskItem] = [
.init(title: "Azure exam preparation",
dueDate: cal.date(byAdding: .day, value: -7, to: Date()),
quadrant: .urgent, category: .custom, taskColor: .orange),
.init(title: "Leona's Birthday",
dueDate: md(6, 17), quadrant: .urgent, category: .birthday, taskColor: .orange, isRecurring: true),
.init(title: "Provoc.ug Domain Expiry",
dueDate: md(7, 3), quadrant: .urgent, category: .domain, taskColor: .blue),
.init(title: "Farouk's Birthday",
dueDate: md(11, 8), quadrant: .delegate_, category: .birthday, taskColor: .green, isRecurring: true),
.init(title: "Enoch's Birthday",
dueDate: md(2, 23, 2027), quadrant: .schedule, category: .annual, taskColor: .yellow, isRecurring: true),
.init(title: "Dad's birthday",
dueDate: md(6, 8), quadrant: .delegate_, category: .birthday, taskColor: .blue, isRecurring: true),
.init(title: "Moms bd",
dueDate: md(2, 15, 2027), quadrant: .delegate_, category: .birthday, taskColor: .blue, isRecurring: true),
.init(title: "Amooti's birthday",
dueDate: md(6, 26), quadrant: .urgent, category: .birthday, taskColor: .orange, isRecurring: true),
.init(title: "Provoc hosting",
dueDate: md(11, 25, 2027), quadrant: .delegate_, category: .domain, taskColor: .blue),
.init(title: "DV lottery preps",
dueDate: md(9, 30, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
.init(title: "Catch up Umutoni",
dueDate: md(11, 15, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
.init(title: "Siraj wedding preps",
dueDate: md(8, 24, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
]
private let cal = Calendar.current
private func md(_ month: Int, _ day: Int, _ year: Int = 2026) -> Date {
var c = DateComponents(); c.year = year; c.month = month; c.day = day
return cal.date(from: c) ?? Date()
}