learning_ai_clock/ios/ChronoMind/Shared/AppGroup/SharedTimerData.swift

184 lines
5.7 KiB
Swift

// Shared Timer Data (App Groups)
// Bridges timer data between iOS app, watchOS, and WidgetKit
// Uses App Group UserDefaults suite for cross-process persistence
import Foundation
// MARK: - App Group Constants
enum AppGroupConstants {
static let suiteName = "group.com.chronomind.shared"
static let timersKey = "chronomind-timers"
static let activeTimerKey = "chronomind-active-timer"
static let lastUpdateKey = "chronomind-last-update"
}
// MARK: - Shared Timer Snapshot (lightweight for widgets/watch)
struct TimerSnapshot: Codable, Identifiable, Equatable {
let id: String
let label: String
let type: CMTimerType
let urgency: UrgencyLevel
let state: CMTimerState
let targetTime: Date
let duration: TimeInterval?
let startedAt: Date?
let elapsedBeforePause: TimeInterval
let snoozeCount: Int
let category: String?
// Pomodoro
let pomodoroCurrentRound: Int?
let pomodoroTotalRounds: Int?
let pomodoroIsBreak: Bool?
// Cascade
let nextWarningTime: Date?
let totalWarnings: Int
let firedWarnings: Int
static func == (lhs: TimerSnapshot, rhs: TimerSnapshot) -> Bool {
lhs.id == rhs.id
}
}
// MARK: - Snapshot Conversion
extension CMTimer {
/// Convert full timer to lightweight snapshot for widgets/watch
func toSnapshot() -> TimerSnapshot {
let nextWarning = warnings.first(where: { !$0.fired })?.scheduledTime
let firedCount = warnings.filter { $0.fired }.count
return TimerSnapshot(
id: id,
label: label,
type: type,
urgency: urgency,
state: state,
targetTime: targetTime,
duration: duration,
startedAt: startedAt,
elapsedBeforePause: elapsedBeforePause,
snoozeCount: snoozeCount,
category: category,
pomodoroCurrentRound: pomodoroState?.currentRound,
pomodoroTotalRounds: pomodoroConfig?.rounds,
pomodoroIsBreak: pomodoroState?.isBreak,
nextWarningTime: nextWarning,
totalWarnings: warnings.count,
firedWarnings: firedCount
)
}
}
// MARK: - Shared Data Manager
final class SharedTimerDataManager {
static let shared = SharedTimerDataManager()
private let defaults: UserDefaults?
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
private init() {
defaults = UserDefaults(suiteName: AppGroupConstants.suiteName)
}
// MARK: - Write (from iOS app)
/// Write full timer list as snapshots to App Group
func writeTimers(_ timers: [CMTimer]) {
let snapshots = timers.map { $0.toSnapshot() }
writeSnapshots(snapshots)
}
/// Write snapshots directly
func writeSnapshots(_ snapshots: [TimerSnapshot]) {
guard let data = try? encoder.encode(snapshots) else { return }
defaults?.set(data, forKey: AppGroupConstants.timersKey)
defaults?.set(Date(), forKey: AppGroupConstants.lastUpdateKey)
}
/// Write the currently active/next-firing timer for quick widget access
func writeActiveTimer(_ timer: CMTimer?) {
if let timer = timer {
let snapshot = timer.toSnapshot()
guard let data = try? encoder.encode(snapshot) else { return }
defaults?.set(data, forKey: AppGroupConstants.activeTimerKey)
} else {
defaults?.removeObject(forKey: AppGroupConstants.activeTimerKey)
}
}
// MARK: - Read (from widgets/watch)
/// Read all timer snapshots from App Group
func readSnapshots() -> [TimerSnapshot] {
guard let data = defaults?.data(forKey: AppGroupConstants.timersKey),
let snapshots = try? decoder.decode([TimerSnapshot].self, from: data) else {
return []
}
return snapshots
}
/// Read the active/next timer snapshot
func readActiveTimer() -> TimerSnapshot? {
guard let data = defaults?.data(forKey: AppGroupConstants.activeTimerKey),
let snapshot = try? decoder.decode(TimerSnapshot.self, from: data) else {
return nil
}
return snapshot
}
/// Last time data was updated by the iOS app
func lastUpdateTime() -> Date? {
defaults?.object(forKey: AppGroupConstants.lastUpdateKey) as? Date
}
// MARK: - Active Timers (filtered)
/// Get only active timer snapshots sorted by target time
func readActiveSnapshots() -> [TimerSnapshot] {
readSnapshots()
.filter { [.active, .warning, .snoozed, .firing].contains($0.state) }
.sorted { $0.targetTime < $1.targetTime }
}
/// Get the next firing timer (active or warning, earliest target)
func readNextFiringTimer() -> TimerSnapshot? {
readSnapshots()
.filter { [.active, .warning].contains($0.state) }
.sorted { $0.targetTime < $1.targetTime }
.first
}
}
// MARK: - TimerSnapshot Helpers
extension TimerSnapshot {
/// Remaining seconds from now
func remainingSeconds(now: Date = Date()) -> TimeInterval {
if state == .paused {
return (duration ?? 0) - elapsedBeforePause
}
return max(0, targetTime.timeIntervalSince(now))
}
/// Whether this timer has fired (past target time)
func hasFired(now: Date = Date()) -> Bool {
now >= targetTime
}
/// Urgency color hex string
var urgencyColorHex: String {
getUrgencyConfig(urgency).colorHex
}
/// Compact remaining time string
func remainingCompact(now: Date = Date()) -> String {
formatDurationCompact(remainingSeconds(now: now))
}
}