learning_ai_clock/ios/ChronoMind/Shared/Store/TimerStore.swift

189 lines
5.2 KiB
Swift

// 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
}
}