Implemented the resumed interrupted recordings and allowed processing discards on the RecordingViewModel view model in the Recording package target.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
/// A view that drives a recording session.
|
||||
@@ -11,17 +11,20 @@ import UIKit
|
||||
///
|
||||
/// All state and control behavior lives in the view's ``Model``; the view itself only renders it and forwards button presses.
|
||||
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
|
||||
@Environment(\.openURL)
|
||||
private var openURL
|
||||
|
||||
/// The font size of the timer's label, scaled relative to the current dynamic type size.
|
||||
@ScaledMetric
|
||||
private var fontSize = Constant.Size.timer
|
||||
|
||||
/// The model that owns the recording state and drives the view's controls.
|
||||
@State private var model: Model
|
||||
|
||||
/// The font size of the timer's label, scaled relative to the current dynamic type size.
|
||||
@ScaledMetric private var fontSize = Constant.Size.timer
|
||||
@State
|
||||
private var model: Model
|
||||
|
||||
/// The closure invoked with the transcription of every processed recording.
|
||||
private let onTranscription: (Transcription) -> Void
|
||||
@@ -69,10 +72,10 @@ public struct RecordingView: View {
|
||||
.easeInOut,
|
||||
value: model.textTimer
|
||||
)
|
||||
.accessibilityLabel(Constant.Text.labelTimer)
|
||||
.accessibilityLabel(.viewRecordingTimerLabel)
|
||||
.accessibilityValue(model.textTimerAccessible)
|
||||
}
|
||||
|
||||
|
||||
GlassEffectContainer {
|
||||
HStack(
|
||||
spacing: Constant.Spacing.stack
|
||||
@@ -87,7 +90,7 @@ public struct RecordingView: View {
|
||||
invertStyle: true
|
||||
))
|
||||
.disabled(model.shouldDisableMain)
|
||||
.accessibilityLabel(labelMain)
|
||||
.accessibilityLabel(model.labelMain)
|
||||
|
||||
if model.shouldShowActions {
|
||||
Button {
|
||||
@@ -104,19 +107,18 @@ public struct RecordingView: View {
|
||||
invertStyle: !model.isProcessing
|
||||
))
|
||||
.disabled(model.isProcessing)
|
||||
.accessibilityLabel(labelSend)
|
||||
|
||||
.accessibilityLabel(model.labelSend)
|
||||
|
||||
Button {
|
||||
model.pressedDiscard()
|
||||
} label: {
|
||||
Image(systemName: Constant.Symbol.discard)
|
||||
Image.Icon.discard
|
||||
.resizable()
|
||||
}
|
||||
.buttonStyle(.recording(
|
||||
invertStyle: true
|
||||
))
|
||||
.disabled(model.shouldDisableMain)
|
||||
.accessibilityLabel(Constant.Text.labelDiscard)
|
||||
.accessibilityLabel(.viewRecordingButtonDiscardLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,72 +136,32 @@ public struct RecordingView: View {
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
Constant.Text.titleError,
|
||||
isPresented: .init(
|
||||
get: { model.error != nil },
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
model.dismissedError()
|
||||
}
|
||||
.viewRecordingAlertErrorTitle,
|
||||
isPresented: .init {
|
||||
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) {
|
||||
if error == .permissionDenied, let url = URL.settings {
|
||||
Button(.viewRecordingAlertErrorButtonSettings) {
|
||||
openURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
Constant.Text.buttonOK,
|
||||
.viewRecordingAlertErrorButtonOk,
|
||||
role: .cancel
|
||||
) {
|
||||
// Dismissal is handled by the presentation binding.
|
||||
}
|
||||
} message: { error in
|
||||
Text(message(for: error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension RecordingView {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The accessibility label for the main button, matching its action in the current state.
|
||||
var labelMain: String {
|
||||
switch model.state {
|
||||
case .notRecording, .processing: Constant.Text.labelRecord
|
||||
case .recording: Constant.Text.labelPause
|
||||
case .paused: Constant.Text.labelResume
|
||||
}
|
||||
}
|
||||
|
||||
/// The accessibility label for the send button: the sending action while paused, the ongoing processing otherwise.
|
||||
var labelSend: String {
|
||||
model.isProcessing
|
||||
? Constant.Text.labelProcessing
|
||||
: Constant.Text.labelSend
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// 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 .assetsUnavailable: Constant.Text.messageAssetsUnavailable
|
||||
case .captureFailed: Constant.Text.messageCaptureFailed
|
||||
case .permissionDenied: Constant.Text.messagePermissionDenied
|
||||
case .transcriptionFailed: Constant.Text.messageTranscriptionFailed
|
||||
} message: { _ in
|
||||
if let message = model.textAlertMessage {
|
||||
Text(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,98 +181,29 @@ private enum Constant {
|
||||
/// The spacing between the elements of a stack.
|
||||
static let stack: CGFloat = 16
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image+Constants
|
||||
|
||||
private extension Image {
|
||||
/// The symbol constants.
|
||||
enum Symbol {
|
||||
enum Icon {
|
||||
/// The system symbol of the discard button's image.
|
||||
static let discard = "trash"
|
||||
static let discard = Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 accessibility label of the discard button.
|
||||
static let labelDiscard = String(
|
||||
localized: "view.recording.button.discard.label",
|
||||
bundle: .module
|
||||
)
|
||||
/// The accessibility label of the main button while recording.
|
||||
static let labelPause = String(
|
||||
localized: "view.recording.button.main.label.pause",
|
||||
bundle: .module
|
||||
)
|
||||
/// The accessibility label of the send button while processing.
|
||||
static let labelProcessing = String(
|
||||
localized: "view.recording.button.send.label.processing",
|
||||
bundle: .module
|
||||
)
|
||||
/// The accessibility label of the main button while not recording.
|
||||
static let labelRecord = String(
|
||||
localized: "view.recording.button.main.label.record",
|
||||
bundle: .module
|
||||
)
|
||||
/// The accessibility label of the main button while paused.
|
||||
static let labelResume = String(
|
||||
localized: "view.recording.button.main.label.resume",
|
||||
bundle: .module
|
||||
)
|
||||
/// The accessibility label of the send button while paused.
|
||||
static let labelSend = String(
|
||||
localized: "view.recording.button.send.label.send",
|
||||
bundle: .module
|
||||
)
|
||||
/// The accessibility label of the timer.
|
||||
static let labelTimer = String(
|
||||
localized: "view.recording.timer.label",
|
||||
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 unavailable speech model assets.
|
||||
static let messageAssetsUnavailable = String(
|
||||
localized: "view.recording.alert.error.message.assets",
|
||||
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
|
||||
)
|
||||
}
|
||||
// MARK: - URL+Constants
|
||||
|
||||
/// 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
|
||||
}()
|
||||
}
|
||||
private extension URL {
|
||||
/// The location that opens the app's settings, where the microphone permission can be granted.
|
||||
static let settings: URL? = {
|
||||
#if os(macOS)
|
||||
.init(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")
|
||||
#else
|
||||
.init(string: UIApplication.openSettingsURLString)
|
||||
#endif
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
Reference in New Issue
Block a user