feat(live-activity): add ActivityKit attributes and LiveActivityManager for Dynamic Island
This commit is contained in:
parent
46d9866253
commit
4b1cbcf81b
145
ios/ChronoMind/LiveActivity/TimerActivityAttributes.swift
Normal file
145
ios/ChronoMind/LiveActivity/TimerActivityAttributes.swift
Normal file
@ -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<TimerActivityAttributes>?
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user