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
@@ -1,5 +1,9 @@
import SwiftUI
#if canImport(UIKit)
import UIKit
#endif
/// A view that drives a recording session.
///
/// The view shows a main button that starts, pauses, and resumes a recording, with a label above it displaying the elapsed recording time.
@@ -10,6 +14,9 @@ public struct RecordingView: View {
// MARK: Properties
/// The action that opens a URL, used to open the app's settings from the error alert.
@Environment(\.openURL) private var openURL
/// The model that owns the recording state and drives the view's controls.
@State private var model: Model
@@ -45,7 +52,8 @@ public struct RecordingView: View {
// MARK: Body
/// The content of the view: the timer label above the main and send buttons, with the transcription of every processed recording
/// forwarded to the closure given at initialization.
/// forwarded to the closure given at initialization, and an alert surfacing any error of the recording flow offering to open the
/// app's settings when the microphone permission was denied.
public var body: some View {
VStack(
spacing: Constant.Spacing.stack
@@ -106,6 +114,53 @@ public struct RecordingView: View {
onTranscription(newValue)
}
}
.alert(
Constant.Text.titleError,
isPresented: .init(
get: { model.error != nil },
set: { isPresented in
if !isPresented {
model.dismissedError()
}
}
),
presenting: model.error
) { error in
if error == .permissionDenied, let url = Constant.URLs.settings {
Button(Constant.Text.buttonSettings) {
openURL(url)
}
}
Button(
Constant.Text.buttonOK,
role: .cancel
) {
// Dismissal is handled by the presentation binding.
}
} message: { error in
Text(message(for: error))
}
}
}
// MARK: - Helpers
private extension RecordingView {
/// Returns the message describing the given error for the alert.
///
/// - Parameter error: The error to describe.
/// - Returns: The message describing the error.
func message(
for error: RecordingError
) -> String {
switch error {
case .captureFailed: Constant.Text.messageCaptureFailed
case .permissionDenied: Constant.Text.messagePermissionDenied
case .transcriptionFailed: Constant.Text.messageTranscriptionFailed
}
}
}
@@ -124,6 +179,52 @@ private enum Constant {
/// The spacing between the elements of a stack.
static let stack: CGFloat = 8
}
/// The text constants, localized through the package's string catalog.
enum Text {
/// The title of the alert's dismissal button.
static let buttonOK = String(
localized: "view.recording.alert.error.button.ok",
bundle: .module
)
/// The title of the alert's button that opens the app's settings.
static let buttonSettings = String(
localized: "view.recording.alert.error.button.settings",
bundle: .module
)
/// The message describing a failure of the audio capture.
static let messageCaptureFailed = String(
localized: "view.recording.alert.error.message.capture",
bundle: .module
)
/// The message describing a denied microphone permission.
static let messagePermissionDenied = String(
localized: "view.recording.alert.error.message.permission",
bundle: .module
)
/// The message describing a failure of the transcription.
static let messageTranscriptionFailed = String(
localized: "view.recording.alert.error.message.transcription",
bundle: .module
)
/// The title of the error alert.
static let titleError = String(
localized: "view.recording.alert.error.title",
bundle: .module
)
}
/// The URL constants.
enum URLs {
/// The location that opens the app's settings, where the microphone permission can be granted.
static let settings: URL? = {
#if os(macOS)
URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")
#else
URL(string: UIApplication.openSettingsURLString)
#endif
}()
}
}
// MARK: - Previews