1→// 2→// DataManager.swift 3→// Planner2026-iOS 4→// 5→// Unified data manager with 3-layer backup system 6→// 7→ 8→import Foundation 9→import Combine 10→ 11→@MainActor 12→class DataManager: ObservableObject { 13→ static let shared = DataManager() 14→ 15→ private var isDataLoaded = false 16→ 17→ // MARK: - Debounce System 18→ private var saveDebounceTask: Task? 19→ private let saveDebounceDelay: UInt64 = 500_000_000 // 500ms 20→ private var pendingSaves: Set = [] 21→ 22→ // MARK: - Published Properties 23→ @Published var tasks: [PlannerTask] = [] 24→ @Published var projects: [Project] = [] 25→ @Published var locations: [TaskLocation] = [] 26→ @Published var locationHistory: [LocationHistoryEntry] = [] 27→ @Published var conversations: [AIConversation] = [] 28→ @Published var insights: [AIInsight] = [] 29→ @Published var tags: [Tag] = [] 30→ @Published var schedules: [DaySchedule] = [] 31→ 32→ // Daily Quests & Gamification 33→ @Published var morningRituals: [MorningRitualDay] = [] 34→ @Published var dailyRoutines: [DailyRoutine] = [] 35→ @Published var yearlyGoals: [YearlyGoal] = [] 36→ 37→ // MARK: - UserDefaults Keys (3-layer backup) 38→ private let tasksKey = "Planner2026_Tasks" 39→ private let tasksBackup1Key = "Planner2026_Tasks_backup1" 40→ private let tasksBackup2Key = "Planner2026_Tasks_backup2" 41→ 42→ private let projectsKey = "Planner2026_Projects" 43→ private let projectsBackup1Key = "Planner2026_Projects_backup1" 44→ private let projectsBackup2Key = "Planner2026_Projects_backup2" 45→ 46→ private let locationsKey = "Planner2026_Locations" 47→ private let locationsBackup1Key = "Planner2026_Locations_backup1" 48→ private let locationsBackup2Key = "Planner2026_Locations_backup2" 49→ 50→ private let locationHistoryKey = "Planner2026_LocationHistory" 51→ private let conversationsKey = "Planner2026_Conversations" 52→ private let insightsKey = "Planner2026_Insights" 53→ private let tagsKey = "Planner2026_Tags" 54→ private let schedulesKey = "Planner2026_Schedules" 55→ 56→ // Daily Quests Keys 57→ private let morningRitualsKey = "Planner2026_MorningRituals" 58→ private let dailyRoutinesKey = "Planner2026_DailyRoutines" 59→ private let yearlyGoalsKey = "Planner2026_YearlyGoals" 60→ 61→ // MARK: - Initialization 62→ private init() { 63→ loadAllData() 64→ } 65→ 66→ // MARK: - Load All Data 67→ func loadAllData() { 68→ guard !isDataLoaded else { return } 69→ 70→ tasks = loadWithBackup(key: tasksKey, backup1: tasksBackup1Key, backup2: tasksBackup2Key) ?? [] 71→ projects = loadWithBackup(key: projectsKey, backup1: projectsBackup1Key, backup2: projectsBackup2Key) ?? [] 72→ locations = loadWithBackup(key: locationsKey, backup1: locationsBackup1Key, backup2: locationsBackup2Key) ?? [] 73→ locationHistory = load(key: locationHistoryKey) ?? [] 74→ conversations = load(key: conversationsKey) ?? [] 75→ insights = load(key: insightsKey) ?? [] 76→ tags = load(key: tagsKey) ?? Tag.systemTags 77→ schedules = load(key: schedulesKey) ?? [] 78→ 79→ // Daily Quests data 80→ morningRituals = load(key: morningRitualsKey) ?? [] 81→ dailyRoutines = load(key: dailyRoutinesKey) ?? [] 82→ yearlyGoals = load(key: yearlyGoalsKey) ?? [] 83→ 84→ // First launch sample data 85→ if tasks.isEmpty && projects.isEmpty { 86→ populateSampleData() 87→ } 88→ 89→ isDataLoaded = true 90→ AppLogger.dataManagerInfo("Loaded: \(tasks.count) tasks, \(projects.count) projects, \(locations.count) locations") 91→ } 92→ 93→ // MARK: - Save All Data 94→ func saveAllData() { 95→ saveWithBackup(data: tasks, key: tasksKey, backup1: tasksBackup1Key, backup2: tasksBackup2Key) 96→ saveWithBackup(data: projects, key: projectsKey, backup1: projectsBackup1Key, backup2: projectsBackup2Key) 97→ saveWithBackup(data: locations, key: locationsKey, backup1: locationsBackup1Key, backup2: locationsBackup2Key) 98→ save(data: locationHistory, key: locationHistoryKey) 99→ save(data: conversations, key: conversationsKey) 100→ save(data: insights, key: insightsKey) 101→ save(data: tags, key: tagsKey) 102→ save(data: schedules, key: schedulesKey) 103→ 104→ // Daily Quests 105→ save(data: morningRituals, key: morningRitualsKey) 106→ save(data: dailyRoutines, key: dailyRoutinesKey) 107→ save(data: yearlyGoals, key: yearlyGoalsKey) 108→ 109→ AppLogger.dataManagerInfo("All data saved") 110→ } 111→ 112→ // MARK: - 3-Layer Backup System 113→ private func loadWithBackup(key: String, backup1: String, backup2: String) -> T? { 114→ // Try primary 115→ if let data = UserDefaults.standard.data(forKey: key), 116→ let decoded = try? JSONDecoder().decode(T.self, from: data) { 117→ return decoded 118→ } 119→ 120→ // Try backup 1 121→ if let data = UserDefaults.standard.data(forKey: backup1), 122→ let decoded = try? JSONDecoder().decode(T.self, from: data) { 123→ UserDefaults.standard.set(data, forKey: key) 124→ return decoded 125→ } 126→ 127→ // Try backup 2 128→ if let data = UserDefaults.standard.data(forKey: backup2), 129→ let decoded = try? JSONDecoder().decode(T.self, from: data) { 130→ UserDefaults.standard.set(data, forKey: key) 131→ UserDefaults.standard.set(data, forKey: backup1) 132→ return decoded 133→ } 134→ 135→ return nil 136→ } 137→ 138→ private func saveWithBackup(data: T, key: String, backup1: String, backup2: String) { 139→ guard let encoded = try? JSONEncoder().encode(data) else { return } 140→ 141→ // Cascade backups 142→ if let existing = UserDefaults.standard.data(forKey: key) { 143→ if let backup1Data = UserDefaults.standard.data(forKey: backup1) { 144→ UserDefaults.standard.set(backup1Data, forKey: backup2) 145→ } 146→ UserDefaults.standard.set(existing, forKey: backup1) 147→ } 148→ 149→ UserDefaults.standard.set(encoded, forKey: key) 150→ } 151→ 152→ private func load(key: String) -> T? { 153→ guard let data = UserDefaults.standard.data(forKey: key) else { return nil } 154→ return try? JSONDecoder().decode(T.self, from: data) 155→ } 156→ 157→ private func save(data: T, key: String) { 158→ guard let encoded = try? JSONEncoder().encode(data) else { return } 159→ UserDefaults.standard.set(encoded, forKey: key) 160→ } 161→ 162→ // MARK: - Debounced Save 163→ private func scheduleDebouncedSave(for dataType: String) { 164→ pendingSaves.insert(dataType) 165→ 166→ saveDebounceTask?.cancel() 167→ saveDebounceTask = Task { 168→ try? await Task.sleep(nanoseconds: saveDebounceDelay) 169→ guard !Task.isCancelled else { return } 170→ 171→ for pending in pendingSaves { 172→ switch pending { 173→ case "tasks": 174→ saveWithBackup(data: tasks, key: tasksKey, backup1: tasksBackup1Key, backup2: tasksBackup2Key) 175→ case "projects": 176→ saveWithBackup(data: projects, key: projectsKey, backup1: projectsBackup1Key, backup2: projectsBackup2Key) 177→ case "locations": 178→ saveWithBackup(data: locations, key: locationsKey, backup1: locationsBackup1Key, backup2: locationsBackup2Key) 179→ case "locationHistory": 180→ save(data: locationHistory, key: locationHistoryKey) 181→ case "conversations": 182→ save(data: conversations, key: conversationsKey) 183→ case "insights": 184→ save(data: insights, key: insightsKey) 185→ case "tags": 186→ save(data: tags, key: tagsKey) 187→ case "schedules": 188→ save(data: schedules, key: schedulesKey) 189→ case "morningRituals": 190→ save(data: morningRituals, key: morningRitualsKey) 191→ case "dailyRoutines": 192→ save(data: dailyRoutines, key: dailyRoutinesKey) 193→ case "yearlyGoals": 194→ save(data: yearlyGoals, key: yearlyGoalsKey) 195→ default: break 196→ } 197→ } 198→ pendingSaves.removeAll() 199→ } 200→ } 201→ 202→ // MARK: - Task CRUD 203→ func addTask(_ task: PlannerTask) { 204→ tasks.append(task) 205→ #if os(iOS) 206→ HapticManager.taskAdded() 207→ #elseif os(macOS) 208→ FeedbackManager.taskAdded() 209→ #endif 210→ scheduleDebouncedSave(for: "tasks") 211→ } 212→ 213→ func updateTask(_ task: PlannerTask) { 214→ if let index = tasks.firstIndex(where: { $0.id == task.id }) { 215→ var updated = task 216→ updated.modifiedAt = Date() 217→ tasks[index] = updated 218→ scheduleDebouncedSave(for: "tasks") 219→ } 220→ } 221→ 222→ func deleteTask(_ task: PlannerTask) { 223→ tasks.removeAll { $0.id == task.id } 224→ #if os(iOS) 225→ HapticManager.taskDeleted() 226→ #elseif os(macOS) 227→ FeedbackManager.taskDeleted() 228→ #endif 229→ scheduleDebouncedSave(for: "tasks") 230→ } 231→ 232→ func toggleTaskCompletion(_ task: PlannerTask) { 233→ if let index = tasks.firstIndex(where: { $0.id == task.id }) { 234→ tasks[index].status = tasks[index].status == .completed ? .pending : .completed 235→ tasks[index].completedAt = tasks[index].status == .completed ? Date() : nil 236→ tasks[index].modifiedAt = Date() 237→ 238→ if tasks[index].status == .completed { 239→ #if os(iOS) 240→ HapticManager.taskCompleted() 241→ #elseif os(macOS) 242→ FeedbackManager.taskCompleted() 243→ #endif 244→ } else { 245→ #if os(iOS) 246→ HapticManager.impact(.light) 247→ #elseif os(macOS) 248→ FeedbackManager.selection() 249→ #endif 250→ } 251→ 252→ scheduleDebouncedSave(for: "tasks") 253→ } 254→ } 255→ 256→ func tasksForProject(_ projectId: UUID) -> [PlannerTask] { 257→ tasks.filter { $0.projectId == projectId } 258→ } 259→ 260→ func tasksForLocation(_ locationId: UUID) -> [PlannerTask] { 261→ tasks.filter { $0.locationId == locationId } 262→ } 263→ 264→ // MARK: - Project CRUD 265→ func addProject(_ project: Project) { 266→ projects.append(project) 267→ #if os(iOS) 268→ HapticManager.impact(.medium) 269→ #elseif os(macOS) 270→ FeedbackManager.taskAdded() 271→ #endif 272→ scheduleDebouncedSave(for: "projects") 273→ } 274→ 275→ func updateProject(_ project: Project) { 276→ if let index = projects.firstIndex(where: { $0.id == project.id }) { 277→ var updated = project 278→ updated.modifiedAt = Date() 279→ projects[index] = updated 280→ scheduleDebouncedSave(for: "projects") 281→ } 282→ } 283→ 284→ func deleteProject(_ project: Project) { 285→ // Remove project reference from tasks 286→ for i in tasks.indices where tasks[i].projectId == project.id { 287→ tasks[i].projectId = nil 288→ } 289→ projects.removeAll { $0.id == project.id } 290→ scheduleDebouncedSave(for: "projects") 291→ scheduleDebouncedSave(for: "tasks") 292→ } 293→ 294→ func projectProgress(_ project: Project) -> Double { 295→ let projectTasks = tasksForProject(project.id) 296→ guard !projectTasks.isEmpty else { return 0.0 } 297→ let completed = projectTasks.filter { $0.status == .completed }.count 298→ return Double(completed) / Double(projectTasks.count) 299→ } 300→ 301→ // MARK: - Location CRUD 302→ func addLocation(_ location: TaskLocation) { 303→ locations.append(location) 304→ #if os(iOS) 305→ HapticManager.impact(.medium) 306→ #elseif os(macOS) 307→ FeedbackManager.taskAdded() 308→ #endif 309→ scheduleDebouncedSave(for: "locations") 310→ } 311→ 312→ func updateLocation(_ location: TaskLocation) { 313→ if let index = locations.firstIndex(where: { $0.id == location.id }) { 314→ locations[index] = location 315→ scheduleDebouncedSave(for: "locations") 316→ } 317→ } 318→ 319→ func deleteLocation(_ location: TaskLocation) { 320→ // Remove location reference from tasks 321→ for i in tasks.indices where tasks[i].locationId == location.id { 322→ tasks[i].locationId = nil 323→ tasks[i].hasGeofenceReminder = false 324→ } 325→ locations.removeAll { $0.id == location.id } 326→ scheduleDebouncedSave(for: "locations") 327→ scheduleDebouncedSave(for: "tasks") 328→ } 329→ 330→ func addLocationHistoryEntry(_ entry: LocationHistoryEntry) { 331→ locationHistory.append(entry) 332→ // Keep only last entries within limit 333→ if locationHistory.count > DataLimits.maxLocationHistory { 334→ locationHistory = Array(locationHistory.suffix(DataLimits.maxLocationHistory)) 335→ } 336→ scheduleDebouncedSave(for: "locationHistory") 337→ } 338→ 339→ // MARK: - AI Data 340→ func saveConversation(_ conversation: AIConversation) { 341→ if let index = conversations.firstIndex(where: { $0.id == conversation.id }) { 342→ conversations[index] = conversation 343→ } else { 344→ conversations.append(conversation) 345→ } 346→ // Keep only recent conversations within limit 347→ if conversations.count > DataLimits.maxConversations { 348→ conversations = Array(conversations.suffix(DataLimits.maxConversations)) 349→ } 350→ scheduleDebouncedSave(for: "conversations") 351→ } 352→ 353→ func deleteConversation(_ conversation: AIConversation) { 354→ conversations.removeAll { $0.id == conversation.id } 355→ scheduleDebouncedSave(for: "conversations") 356→ } 357→ 358→ func addInsight(_ insight: AIInsight) { 359→ insights.insert(insight, at: 0) 360→ // Keep only recent insights within limit 361→ if insights.count > DataLimits.maxInsights { 362→ insights = Array(insights.prefix(DataLimits.maxInsights)) 363→ } 364→ scheduleDebouncedSave(for: "insights") 365→ } 366→ 367→ func markInsightAsRead(_ insight: AIInsight) { 368→ if let index = insights.firstIndex(where: { $0.id == insight.id }) { 369→ insights[index].isRead = true 370→ scheduleDebouncedSave(for: "insights") 371→ } 372→ } 373→ 374→ func saveSchedule(_ schedule: DaySchedule) { 375→ if let index = schedules.firstIndex(where: { Calendar.current.isDate($0.date, inSameDayAs: schedule.date) }) { 376→ schedules[index] = schedule 377→ } else { 378→ schedules.append(schedule) 379→ } 380→ // Keep only recent schedules within retention period 381→ guard let retentionCutoff = Calendar.current.date(byAdding: .day, value: -DataLimits.schedulesRetentionDays, to: Date()) else { return } 382→ schedules = schedules.filter { $0.date >= retentionCutoff } 383→ scheduleDebouncedSave(for: "schedules") 384→ } 385→ 386→ // MARK: - Statistics 387→ func getTaskStatistics() -> TaskStatistics { 388→ let completed = tasks.filter { $0.status == .completed }.count 389→ let pending = tasks.filter { $0.status == .pending || $0.status == .inProgress }.count 390→ let overdue = tasks.filter { $0.isOverdue }.count 391→ let dueToday = tasks.filter { $0.isDueToday }.count 392→ 393→ return TaskStatistics( 394→ total: tasks.count, 395→ completed: completed, 396→ pending: pending, 397→ overdue: overdue, 398→ dueToday: dueToday 399→ ) 400→ } 401→ 402→ func getUnreadInsightsCount() -> Int { 403→ insights.filter { !$0.isRead && !$0.isExpired }.count 404→ } 405→ 406→ // MARK: - Tasks for Today 407→ var tasksForToday: [PlannerTask] { 408→ let today = Calendar.current.startOfDay(for: Date()) 409→ 410→ return tasks.filter { task in 411→ // Tasks due today 412→ if let dueDate = task.dueDate { 413→ let dueDay = Calendar.current.startOfDay(for: dueDate) 414→ if dueDay == today { 415→ return true 416→ } 417→ } 418→ // Tasks scheduled for today 419→ if let scheduledDate = task.scheduledDate { 420→ let scheduledDay = Calendar.current.startOfDay(for: scheduledDate) 421→ if scheduledDay == today { 422→ return true 423→ } 424→ } 425→ return false 426→ }.sorted { $0.priority.rawValue > $1.priority.rawValue } 427→ } 428→ 429→ // MARK: - Morning Ritual CRUD 430→ func saveMorningRitual(_ ritual: MorningRitualDay) { 431→ if let index = morningRituals.firstIndex(where: { Calendar.current.isDate($0.date, inSameDayAs: ritual.date) }) { 432→ morningRituals[index] = ritual 433→ } else { 434→ morningRituals.insert(ritual, at: 0) 435→ } 436→ // Keep only rituals within retention period 437→ guard let retentionCutoff = Calendar.current.date(byAdding: .day, value: -DataLimits.morningRitualsRetentionDays, to: Date()) else { return } 438→ morningRituals = morningRituals.filter { $0.date >= retentionCutoff } 439→ scheduleDebouncedSave(for: "morningRituals") 440→ } 441→ 442→ func getTodayRitual() -> MorningRitualDay? { 443→ let today = Calendar.current.startOfDay(for: Date()) 444→ return morningRituals.first { Calendar.current.isDate($0.date, inSameDayAs: today) } 445→ } 446→ 447→ func getMorningRitualStreak() -> Int { 448→ var streak = 0 449→ var checkDate = Calendar.current.startOfDay(for: Date()) 450→ 451→ while true { 452→ if let ritual = morningRituals.first(where: { Calendar.current.isDate($0.date, inSameDayAs: checkDate) }), 453→ ritual.isComplete { 454→ streak += 1 455→ guard let previousDay = Calendar.current.date(byAdding: .day, value: -1, to: checkDate) else { break } 456→ checkDate = previousDay 457→ } else { 458→ break 459→ } 460→ } 461→ 462→ return streak 463→ } 464→ 465→ // MARK: - Daily Routine CRUD 466→ func saveDailyRoutine(_ routine: DailyRoutine) { 467→ if let index = dailyRoutines.firstIndex(where: { Calendar.current.isDate($0.date, inSameDayAs: routine.date) }) { 468→ dailyRoutines[index] = routine 469→ } else { 470→ dailyRoutines.insert(routine, at: 0) 471→ } 472→ // Keep only routines within retention period 473→ guard let retentionCutoff = Calendar.current.date(byAdding: .day, value: -DataLimits.dailyRoutinesRetentionDays, to: Date()) else { return } 474→ dailyRoutines = dailyRoutines.filter { $0.date >= retentionCutoff } 475→ scheduleDebouncedSave(for: "dailyRoutines") 476→ } 477→ 478→ func getTodayRoutine() -> DailyRoutine? { 479→ let today = Calendar.current.startOfDay(for: Date()) 480→ return dailyRoutines.first { Calendar.current.isDate($0.date, inSameDayAs: today) } 481→ } 482→ 483→ // MARK: - Yearly Goals CRUD 484→ func addYearlyGoal(_ goal: YearlyGoal) { 485→ yearlyGoals.insert(goal, at: 0) 486→ #if os(iOS) 487→ HapticManager.notification(.success) 488→ #elseif os(macOS) 489→ FeedbackManager.notificationSuccess() 490→ #endif 491→ scheduleDebouncedSave(for: "yearlyGoals") 492→ } 493→ 494→ func updateYearlyGoal(_ goal: YearlyGoal) { 495→ if let index = yearlyGoals.firstIndex(where: { $0.id == goal.id }) { 496→ yearlyGoals[index] = goal 497→ scheduleDebouncedSave(for: "yearlyGoals") 498→ } 499→ } 500→ 501→ func deleteYearlyGoal(_ goal: YearlyGoal) { 502→ yearlyGoals.removeAll { $0.id == goal.id } 503→ scheduleDebouncedSave(for: "yearlyGoals") 504→ } 505→ 506→ func getActiveYearlyGoals() -> [YearlyGoal] { 507→ let currentYear = Calendar.current.component(.year, from: Date()) 508→ return yearlyGoals.filter { $0.year == currentYear && !$0.isArchived } 509→ } 510→ 511→ // MARK: - Sample Data 512→ private func populateSampleData() { 513→ let sampleProject = Project( 514→ name: "Getting Started", 515→ projectDescription: "Sample project to help you get started with Planner 2026", 516→ colorHex: "#007AFF", 517→ icon: "star.fill" 518→ ) 519→ projects.append(sampleProject) 520→ 521→ let sampleTasks = [ 522→ PlannerTask( 523→ title: "Explore the app", 524→ notes: "Check out all the features!", 525→ priority: .medium, 526→ projectId: sampleProject.id 527→ ), 528→ PlannerTask( 529→ title: "Add your first real task", 530→ priority: .high, 531→ dueDate: Calendar.current.date(byAdding: .day, value: 1, to: Date()) 532→ ), 533→ PlannerTask( 534→ title: "Try the AI assistant", 535→ notes: "Ask the AI to help you plan your day", 536→ priority: .low 537→ ) 538→ ] 539→ tasks.append(contentsOf: sampleTasks) 540→ 541→ tags = Tag.systemTags 542→ 543→ saveAllData() 544→ AppLogger.dataManagerInfo("Sample data populated") 545→ } 546→} 547→ 548→// MARK: - Statistics Model 549→struct TaskStatistics { 550→ let total: Int 551→ let completed: Int 552→ let pending: Int 553→ let overdue: Int 554→ let dueToday: Int 555→ 556→ var completionRate: Double { 557→ guard total > 0 else { return 0 } 558→ return Double(completed) / Double(total) * 100 559→ } 560→} 561→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.