#if os(iOS) @preconcurrency import ActivityKit #endif import Commanding import Foundation import Recording /// The reporting service that surfaces the recording flow in a Live Activity on the Lock Screen and in the Dynamic Island. /// /// The service implements the `Recording` feature's `Reporting` port over the Live Activity vocabulary shared with the widget /// extension through the `Commanding` target: it starts a Live Activity when a recording starts, updates its content on every pause, /// resumption, and processing transition, and ends it — dismissing it right away — when the flow ends. The activity is an auxiliary /// surface of the recording flow, never a required one: a start that fails, or that the user disallowed in the system settings, is /// silently ignored, and the platforms without Live Activities reduce the whole service to a no-op. @MainActor final class ActivityReporting: Reporting { // MARK: Properties #if os(iOS) /// The Live Activity reflecting the ongoing recording flow, or `nil` while none is. private var activity: Activity? #endif // MARK: Initializers /// Creates a reporting service with no Live Activity started yet. init() {} // MARK: Methods /// Starts a Live Activity for a new recording, unless the user disallowed Live Activities or the system refuses to start one. /// /// - Parameter anchor: The instant the elapsed recording time counts from. func started( at anchor: Date ) async { #if os(iOS) guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } activity = try? Activity.request( attributes: RecordingActivityAttributes(), content: .init( state: .init( state: .recording, anchor: anchor, elapsed: .zero ), staleDate: nil ) ) #endif } /// Updates the Live Activity with the pause of the ongoing recording, freezing its timer at the elapsed recording time. /// /// - Parameter elapsed: The number of seconds spent recording so far, excluding any time spent paused. func paused( elapsed: TimeInterval ) async { #if os(iOS) await update( state: .paused, anchor: nil, elapsed: elapsed ) #endif } /// Updates the Live Activity with the resumption of a paused recording, restarting its timer from the given anchor. /// /// - Parameter anchor: The instant the elapsed recording time counts from, moved back by the time already spent recording. func resumed( anchor: Date ) async { #if os(iOS) await update( state: .recording, anchor: anchor, elapsed: .zero ) #endif } /// Updates the Live Activity with the processing of the recorded input, freezing its timer at the elapsed recording time. /// /// - Parameter elapsed: The number of seconds spent recording, excluding any time spent paused. func processing( elapsed: TimeInterval ) async { #if os(iOS) await update( state: .processing, anchor: nil, elapsed: elapsed ) #endif } /// Ends the Live Activity of the recording flow, dismissing it right away. func ended() async { #if os(iOS) await activity?.end( activity?.content, dismissalPolicy: .immediate ) activity = nil #endif } } #if os(iOS) // MARK: - Helpers private extension ActivityReporting { /// Updates the dynamic content of the Live Activity, when one is running. /// /// - Parameters: /// - state: The state of the recording flow to show. /// - anchor: The instant the elapsed recording time counts from, or `nil` while the recording is not running. /// - elapsed: The number of seconds spent recording up to the transition, excluding any time spent paused. func update( state: RecordingActivityState, anchor: Date?, elapsed: TimeInterval ) async { await activity?.update( .init( state: .init( state: state, anchor: anchor, elapsed: elapsed ), staleDate: nil ) ) } } #endif