Propagated the Transcription type and locale binding through the RecordingView view and its view model in the Recording package target.

This commit is contained in:
2026-07-04 12:11:24 +02:00
parent 68c070948e
commit 20cbda7b27
6 changed files with 47 additions and 30 deletions
@@ -15,6 +15,9 @@ extension ContentView {
// MARK: Properties
/// The locale of the spoken language to transcribe.
var locale: Locale = .current
/// The transcription currently presented in the modal sheet, or `nil` when none is shown.
var transcription: Transcription?
@@ -28,13 +31,13 @@ extension ContentView {
// MARK: Methods
/// Handles the transcribed text of a processed recording, presenting it in the modal sheet.
/// Handles the transcription of a processed recording, presenting it in the modal sheet.
///
/// - Parameter text: The transcribed text of the processed recording.
/// - Parameter transcription: The transcription of the processed recording.
func received(
_ text: String
_ transcription: Transcription
) {
transcription = .init(text: text)
self.transcription = transcription
}
/// Handles the dismissal of the modal sheet.
+4 -3
View File
@@ -23,9 +23,10 @@ struct ContentView: View {
NavigationStack {
RecordingView(
recorder: model.recorder,
transcriber: model.transcriber
) { text in
model.received(text)
transcriber: model.transcriber,
locale: $model.locale,
) { transcription in
model.received(transcription)
}
.navigationTitle("view.recording.navigation.title")
#if !os(macOS)
@@ -1,7 +1,7 @@
import Foundation
/// A transcription of a processed recording.
public struct Transcription: Identifiable, Sendable {
public struct Transcription: Equatable, Identifiable, Sendable {
// MARK: Properties
@@ -21,7 +21,11 @@ extension RecordingView {
private(set) var elapsedSeconds: Int = 0
/// The transcription of the last processed recording, or `nil` when none has been processed yet.
private(set) var textTranscription: String?
private(set) var transcription: Transcription?
/// The binding to the locale of the spoken language to transcribe.
@ObservationIgnored
private let locale: Binding<Locale>
/// The service that captures the audio from a microphone.
@ObservationIgnored
@@ -42,12 +46,15 @@ extension RecordingView {
/// - Parameters:
/// - recorder: The service that captures the audio from a microphone. Defaults to ``DummyRecordingService``.
/// - transcribe: The callable service that transcribes the recorded audio into text. Defaults to ``DummyTranscribing``.
/// - locale: The binding to the locale of the spoken language to transcribe. Defaults to a constant binding to the user's current locale.
init(
recorder: any RecordingService = DummyRecordingService(),
transcribe: any Transcribing = DummyTranscribing()
transcribe: any Transcribing = DummyTranscribing(),
locale: Binding<Locale> = .constant(.current)
) {
self.recorder = recorder
self.transcribe = transcribe
self.locale = locale
}
// MARK: Computed
@@ -175,7 +182,7 @@ private extension RecordingView.Model {
// MARK: Methods
/// Processes the recorded input: it stops the audio capture, transcribes the captured audio, and publishes the result in ``textTranscription``;
/// 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.
func processInput() async {
guard state == .processing else {
@@ -185,12 +192,12 @@ private extension RecordingView.Model {
do {
let audio = try await recorder.stop()
textTranscription = try await transcribe(
transcription = try await transcribe(
audio,
locale: .current
).text
locale: locale.wrappedValue
)
} catch {
textTranscription = nil
transcription = nil
}
elapsedSeconds = 0
@@ -218,14 +225,14 @@ private extension RecordingView.Model {
/// Starts the timer task, which increments ``elapsedSeconds`` once per second until it is cancelled. Any previously running timer task is cancelled first.
///
/// - Parameter shouldRestartTimer: Whether ``elapsedSeconds`` and ``textTranscription`` should be cleared before the timer starts,
/// - Parameter shouldRestartTimer: Whether ``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.
func startTimer(
_ shouldRestartTimer: Bool
) {
if shouldRestartTimer {
elapsedSeconds = 0
textTranscription = nil
transcription = nil
}
taskTimer?.cancel()
@@ -13,8 +13,8 @@ public struct RecordingView: View {
/// The model that owns the recording state and drives the view's controls.
@State private var model: Model
/// The closure invoked with the transcribed text of every processed recording.
private let onTranscription: (String) -> Void
/// The closure invoked with the transcription of every processed recording.
private let onTranscription: (Transcription) -> Void
// MARK: Initializers
@@ -23,15 +23,18 @@ public struct RecordingView: View {
/// - Parameters:
/// - recorder: The service that captures the audio from a microphone.
/// - transcriber: The service that transcribes the recorded audio into text.
/// - onTranscription: The closure invoked with the transcribed text of every processed recording. Defaults to a closure that does nothing.
/// - locale: The binding to the locale of the spoken language to transcribe.
/// - onTranscription: The closure invoked with the transcription of every processed recording. Defaults to a closure that does nothing.
public init(
recorder: any RecordingService,
transcriber: any Transcribing,
onTranscription: @escaping (String) -> Void = { _ in }
locale: Binding<Locale>,
onTranscription: @escaping (Transcription) -> Void
) {
self.model = .init(
recorder: recorder,
transcribe: transcriber
transcribe: transcriber,
locale: locale
)
self.onTranscription = onTranscription
}
@@ -101,7 +104,7 @@ public struct RecordingView: View {
)
}
.onChange(
of: model.textTranscription,
of: model.transcription,
initial: false
) { _, newValue in
if let newValue {
@@ -130,6 +133,9 @@ private enum Constant {
) {
RecordingView(
recorder: DummyRecordingService(),
transcriber: DummyTranscribing()
)
transcriber: DummyTranscribing(),
locale: .constant(.current)
) { _ in
// On transcription closure.
}
}
@@ -20,7 +20,7 @@ struct RecordingViewModelTests {
#expect(model.state == .notRecording)
#expect(model.elapsedSeconds == 0)
#expect(model.textTimer == "00:00")
#expect(model.textTranscription == nil)
#expect(model.transcription == nil)
}
}
@@ -368,7 +368,7 @@ struct RecordingViewModelTests {
#expect(model.state == .notRecording)
#expect(model.elapsedSeconds == 0)
#expect(model.textTranscription != nil)
#expect(model.transcription != nil)
}
@Test func `clears the transcription when the transcriber fails`() async throws {
@@ -387,7 +387,7 @@ struct RecordingViewModelTests {
#expect(model.state == .notRecording)
#expect(model.elapsedSeconds == 0)
#expect(model.textTranscription == nil)
#expect(model.transcription == nil)
}
@Test func `clears the transcription when a new recording starts`() async throws {
@@ -400,12 +400,12 @@ struct RecordingViewModelTests {
try await Task.sleep(for: .seconds(0.5))
#expect(model.textTranscription != nil)
#expect(model.transcription != nil)
model.state = .recording
model.updatedState(shouldRestartTimer: true)
#expect(model.textTranscription == nil)
#expect(model.transcription == nil)
model.state = .notRecording
model.updatedState(shouldRestartTimer: false)