From 20cbda7b27a7571eb566a749e3b11190c087f716 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 4 Jul 2026 12:11:24 +0200 Subject: [PATCH] Propagated the Transcription type and locale binding through the RecordingView view and its view model in the Recording package target. --- .../View Models/ContentViewModel.swift | 11 +++++--- Apps/Attendi/Views/ContentView.swift | 7 +++--- .../Recording/Models/Transcription.swift | 2 +- .../View Models/RecordingViewModel.swift | 25 ++++++++++++------- .../Recording/Views/RecordingView.swift | 22 ++++++++++------ .../View Models/RecordingViewModelTests.swift | 10 ++++---- 6 files changed, 47 insertions(+), 30 deletions(-) diff --git a/Apps/Attendi/View Models/ContentViewModel.swift b/Apps/Attendi/View Models/ContentViewModel.swift index aeef487..c8009c1 100644 --- a/Apps/Attendi/View Models/ContentViewModel.swift +++ b/Apps/Attendi/View Models/ContentViewModel.swift @@ -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. diff --git a/Apps/Attendi/Views/ContentView.swift b/Apps/Attendi/Views/ContentView.swift index f30e84c..bf8d9c0 100644 --- a/Apps/Attendi/Views/ContentView.swift +++ b/Apps/Attendi/Views/ContentView.swift @@ -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) diff --git a/Packages/Features/Sources/Recording/Models/Transcription.swift b/Packages/Features/Sources/Recording/Models/Transcription.swift index 776a530..16931c4 100644 --- a/Packages/Features/Sources/Recording/Models/Transcription.swift +++ b/Packages/Features/Sources/Recording/Models/Transcription.swift @@ -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 diff --git a/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift b/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift index 6f7c26e..2e0b5a4 100644 --- a/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift +++ b/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift @@ -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 /// 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 = .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() diff --git a/Packages/Features/Sources/Recording/Views/RecordingView.swift b/Packages/Features/Sources/Recording/Views/RecordingView.swift index 9bf5aee..5b90492 100644 --- a/Packages/Features/Sources/Recording/Views/RecordingView.swift +++ b/Packages/Features/Sources/Recording/Views/RecordingView.swift @@ -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, + 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. + } } diff --git a/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift b/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift index 1ccde6b..47f7263 100644 --- a/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift +++ b/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift @@ -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)