From 4b1cbcf81b3a398b5bdc652b227c3d7d62e2f210 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 27 Feb 2026 22:01:28 -0800 Subject: [PATCH] feat(live-activity): add ActivityKit attributes and LiveActivityManager for Dynamic Island --- .../TimerActivityAttributes.swift | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 ios/ChronoMind/LiveActivity/TimerActivityAttributes.swift diff --git a/ios/ChronoMind/LiveActivity/TimerActivityAttributes.swift b/ios/ChronoMind/LiveActivity/TimerActivityAttributes.swift new file mode 100644 index 0000000..7f81a41 --- /dev/null +++ b/ios/ChronoMind/LiveActivity/TimerActivityAttributes.swift @@ -0,0 +1,145 @@ +// ── Live Activity Attributes ────────────────────────────────── +// ActivityKit data model for Dynamic Island + Lock Screen Live Activities +// Shared between main app and widget extension + +import Foundation +import ActivityKit + +// MARK: - Timer Live Activity + +struct TimerActivityAttributes: ActivityAttributes { + /// Static data that doesn't change during the activity + struct ContentState: Codable, Hashable { + let remainingSeconds: Int + let targetTime: Date + let state: String // CMTimerState raw value + let urgencyHex: String // color hex for urgency + let pomodoroRound: Int? // current round (nil if not pomodoro) + let pomodoroTotal: Int? // total rounds + let isBreak: Bool // pomodoro break phase + } + + // Fixed for the lifetime of the activity + let timerId: String + let timerLabel: String + let timerType: String // CMTimerType raw value + let urgency: String // UrgencyLevel raw value + let createdAt: Date +} + +// MARK: - Activity Manager + +@MainActor +final class LiveActivityManager { + static let shared = LiveActivityManager() + + private var currentActivity: Activity? + + private init() {} + + // MARK: - Start + + /// Start a Live Activity for an active timer + func startActivity(for timer: CMTimer) { + guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } + + // End any existing activity first + endActivity() + + let urgencyConfig = getUrgencyConfig(timer.urgency) + let remaining = Int(max(0, timer.targetTime.timeIntervalSinceNow)) + + let attributes = TimerActivityAttributes( + timerId: timer.id, + timerLabel: timer.label, + timerType: timer.type.rawValue, + urgency: timer.urgency.rawValue, + createdAt: timer.createdAt + ) + + let state = TimerActivityAttributes.ContentState( + remainingSeconds: remaining, + targetTime: timer.targetTime, + state: timer.state.rawValue, + urgencyHex: urgencyConfig.colorHex, + pomodoroRound: timer.pomodoroState?.currentRound, + pomodoroTotal: timer.pomodoroConfig?.rounds, + isBreak: timer.pomodoroState?.isBreak ?? false + ) + + let content = ActivityContent(state: state, staleDate: timer.targetTime.addingTimeInterval(60)) + + do { + currentActivity = try Activity.request( + attributes: attributes, + content: content, + pushType: nil + ) + } catch { + // Live Activity not available — silent fallback + } + } + + // MARK: - Update + + /// Update the Live Activity with new timer state + func updateActivity(for timer: CMTimer) { + guard let activity = currentActivity, activity.activityState == .active else { + // If no active activity but timer is active, start one + if [.active, .warning].contains(timer.state) { + startActivity(for: timer) + } + return + } + + let urgencyConfig = getUrgencyConfig(timer.urgency) + let remaining = Int(max(0, timer.targetTime.timeIntervalSinceNow)) + + let state = TimerActivityAttributes.ContentState( + remainingSeconds: remaining, + targetTime: timer.targetTime, + state: timer.state.rawValue, + urgencyHex: urgencyConfig.colorHex, + pomodoroRound: timer.pomodoroState?.currentRound, + pomodoroTotal: timer.pomodoroConfig?.rounds, + isBreak: timer.pomodoroState?.isBreak ?? false + ) + + let content = ActivityContent(state: state, staleDate: timer.targetTime.addingTimeInterval(60)) + + Task { + await activity.update(content) + } + } + + // MARK: - End + + /// End the current Live Activity + func endActivity() { + guard let activity = currentActivity else { return } + + let finalState = TimerActivityAttributes.ContentState( + remainingSeconds: 0, + targetTime: Date(), + state: CMTimerState.dismissed.rawValue, + urgencyHex: "#5B8DEE", + pomodoroRound: nil, + pomodoroTotal: nil, + isBreak: false + ) + + let content = ActivityContent(state: finalState, staleDate: nil) + + Task { + await activity.end(content, dismissalPolicy: .immediate) + } + currentActivity = nil + } + + /// End activity for a specific timer ID + func endActivity(for timerId: String) { + guard let activity = currentActivity, + activity.attributes.timerId == timerId else { return } + endActivity() + } +}