Surfaced recording flow errors to the user through an alert in the Recording package target.

This commit is contained in:
2026-07-04 15:40:03 +02:00
parent 088c4c757e
commit 0ca948272b
6 changed files with 315 additions and 10 deletions
@@ -19,7 +19,10 @@ extension RecordingView {
/// The number of seconds spent recording, excluding any time spent paused.
private(set) var elapsedSeconds: Int = 0
/// The error to surface to the user, or `nil` when none occurred. Cleared when it is dismissed or a new recording starts.
private(set) var error: RecordingError?
/// The transcription of the last processed recording, or `nil` when none has been processed yet.
private(set) var transcription: Transcription?
@@ -149,6 +152,11 @@ extension RecordingView {
}
}
/// Handles the dismissal of the surfaced ``error``, clearing it.
func dismissedError() {
error = nil
}
/// Handles a press of the send button.
///
/// Moves a paused recording into processing, stopping the timer and kicking off ``processInput()``. Does nothing in any other state.
@@ -188,7 +196,8 @@ private extension RecordingView.Model {
// MARK: Methods
/// Processes the recorded input: it stops the audio capture, transcribes the captured audio, and publishes the result in ``transcription``;
/// when finished or when either service fails it resets ``elapsedSeconds`` and returns the model to the not-recording state.
/// when finished or when either service fails, in which case the failure is published in ``error`` it resets ``elapsedSeconds``
/// and returns the model to the not-recording state.
func processInput() async {
guard state == .processing else {
return
@@ -197,12 +206,18 @@ private extension RecordingView.Model {
do {
let audio = try await capturer.stop()
transcription = try await transcribe(
audio,
locale: locale.wrappedValue
)
do {
transcription = try await transcribe(
audio,
locale: locale.wrappedValue
)
} catch {
transcription = nil
self.error = .transcriptionFailed
}
} catch {
transcription = nil
self.error = .captureFailed
}
accumulated = .zero
@@ -257,7 +272,8 @@ private extension RecordingView.Model {
}
}
/// Starts the audio capture through the attached ``Capturing``, falling back to the not-recording state when the service fails.
/// Starts the audio capture through the attached ``Capturing``, falling back to the not-recording state and publishing the failure
/// in ``error`` when the service fails.
///
/// - Parameter isNewRecording: Whether a new recording should be started, as opposed to a paused one being resumed.
func startCapturer(
@@ -272,6 +288,9 @@ private extension RecordingView.Model {
self.stopTimer()
self.state = .notRecording
self.error = error as? AudioCapturingError == .permissionNotGranted
? .permissionDenied
: .captureFailed
}
}
}
@@ -279,14 +298,15 @@ private extension RecordingView.Model {
/// Starts the timer task, which updates ``elapsedSeconds`` from the time measured by the clock on every whole second until it is
/// cancelled, so scheduling latency never accumulates as drift. Any previously running timer task is cancelled first.
///
/// - Parameter shouldRestartTimer: Whether the measured recording time, ``elapsedSeconds``, and ``transcription`` should be
/// cleared before the timer starts, which is the case for a new recording as opposed to one resuming from a pause.
/// - Parameter shouldRestartTimer: Whether the measured recording time, ``elapsedSeconds``, ``error``, and ``transcription``
/// should be cleared before the timer starts, which is the case for a new recording as opposed to one resuming from a pause.
func startTimer(
_ shouldRestartTimer: Bool
) {
if shouldRestartTimer {
accumulated = .zero
elapsedSeconds = 0
error = nil
transcription = nil
}