// ── Timer Store ──────────────────────────────────────────────── // Observable store managing all timers — equivalent of Zustand store // Persists to UserDefaults (SwiftData migration in future) import Foundation import Combine @MainActor final class TimerStore: ObservableObject { // MARK: - Published State @Published var timers: [CMTimer] = [] @Published var now: Date = Date() // MARK: - Private private var tickTimer: Timer? private let persistenceKey = "chronomind-timers" private let notifications = CMNotificationManager.shared // MARK: - Init init() { loadTimers() startTicking() } deinit { tickTimer?.invalidate() } // MARK: - CRUD func addAlarm(_ params: CreateAlarmParams) -> CMTimer { let timer = createAlarm(params) timers.append(timer) notifications.scheduleNotifications(for: timer) saveTimers() return timer } func addCountdown(_ params: CreateCountdownParams) -> CMTimer { let timer = createCountdown(params) timers.append(timer) notifications.scheduleNotifications(for: timer) saveTimers() return timer } func addPomodoro(_ params: CreatePomodoroParams = CreatePomodoroParams()) -> CMTimer { let timer = createPomodoro(params) timers.append(timer) notifications.scheduleNotifications(for: timer) saveTimers() return timer } func removeTimer(_ id: String) { timers.removeAll { $0.id == id } notifications.removeNotifications(for: id) saveTimers() } // MARK: - State Transitions func pause(_ id: String) { updateTimer(id) { pauseTimer($0) } } func resume(_ id: String) { updateTimer(id) { t in let resumed = resumeTimer(t) self.notifications.scheduleNotifications(for: resumed) return resumed } } func fire(_ id: String) { updateTimer(id) { fireTimer($0) } } func snooze(_ id: String, minutes: Int) { updateTimer(id) { t in let snoozed = snoozeTimer(t, snoozeMinutes: minutes) self.notifications.scheduleNotifications(for: snoozed) return snoozed } } func dismiss(_ id: String) { updateTimer(id) { dismissTimer($0) } } func complete(_ id: String) { updateTimer(id) { completeTimer($0) } } func advancePom(_ id: String) { guard let index = timers.firstIndex(where: { $0.id == id }) else { return } if let next = advancePomodoro(timers[index]) { timers[index] = next notifications.scheduleNotifications(for: next) saveTimers() } } // MARK: - Tick func tick() { let currentTime = Date() now = currentTime var changed = false for i in timers.indices { // Check if timer should fire if shouldTimerFire(timers[i], now: currentTime) { timers[i] = fireTimer(timers[i]) changed = true // Haptic feedback HapticEngine.fire(urgency: timers[i].urgency) continue } // Check cascade warnings let newlyFired = checkWarnings(&timers[i].warnings, now: currentTime) if !newlyFired.isEmpty { changed = true // Update state to warning if still active if timers[i].state == .active { timers[i].state = .warning } // Haptic feedback for warning HapticEngine.warning(urgency: timers[i].urgency) } } if changed { saveTimers() } } // MARK: - Queries func getTimer(_ id: String) -> CMTimer? { timers.first { $0.id == id } } var activeTimers: [CMTimer] { timers.filter { [.active, .warning, .snoozed, .paused, .firing].contains($0.state) } } var nextFiringTimer: CMTimer? { timers .filter { [.active, .warning].contains($0.state) } .sorted { $0.targetTime < $1.targetTime } .first } // MARK: - Private Helpers private func updateTimer(_ id: String, updater: (CMTimer) -> CMTimer) { guard let index = timers.firstIndex(where: { $0.id == id }) else { return } timers[index] = updater(timers[index]) saveTimers() } private func startTicking() { tickTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in Task { @MainActor in self?.tick() } } } // MARK: - Persistence (UserDefaults for now, SwiftData later) private func saveTimers() { guard let data = try? JSONEncoder().encode(timers) else { return } UserDefaults.standard.set(data, forKey: persistenceKey) } private func loadTimers() { guard let data = UserDefaults.standard.data(forKey: persistenceKey), let saved = try? JSONDecoder().decode([CMTimer].self, from: data) else { return } timers = saved } }