From be130eb05f90943f1fb6334345a39f9f8d65b8ec Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 4 Jul 2026 13:41:56 +0200 Subject: [PATCH] Renamed the RecordingService protocol in the Recording package target as Capturing. --- .../View Models/ContentViewModel.swift | 4 +- Apps/Attendi/Views/ContentView.swift | 2 +- ...RecordingService.swift => Capturing.swift} | 2 +- ...dingService.swift => AudioCapturing.swift} | 30 ++++++------- ...dingService.swift => DummyCapturing.swift} | 2 +- .../View Models/RecordingViewModel.swift | 24 +++++------ .../Recording/Views/RecordingView.swift | 8 ++-- .../View Models/RecordingViewModelTests.swift | 42 +++++++++---------- 8 files changed, 57 insertions(+), 57 deletions(-) rename Packages/Features/Sources/Recording/Protocols/{RecordingService.swift => Capturing.swift} (94%) rename Packages/Features/Sources/Recording/Services/{AudioRecordingService.swift => AudioCapturing.swift} (80%) rename Packages/Features/Sources/Recording/Services/{DummyRecordingService.swift => DummyCapturing.swift} (93%) diff --git a/Apps/Attendi/View Models/ContentViewModel.swift b/Apps/Attendi/View Models/ContentViewModel.swift index cc89d41..cb2ad8a 100644 --- a/Apps/Attendi/View Models/ContentViewModel.swift +++ b/Apps/Attendi/View Models/ContentViewModel.swift @@ -28,7 +28,7 @@ extension ContentView { /// The service that captures the audio from the device's microphone. @ObservationIgnored - let recorder: AudioRecordingService + let capturer: AudioCapturing /// The service that transcribes the recorded audio into text on device. @ObservationIgnored @@ -41,7 +41,7 @@ extension ContentView { init() { self.locale = .current self.locales = [] - self.recorder = .init() + self.capturer = .init() self.transcriber = .init() } diff --git a/Apps/Attendi/Views/ContentView.swift b/Apps/Attendi/Views/ContentView.swift index b92cb38..b4dca85 100644 --- a/Apps/Attendi/Views/ContentView.swift +++ b/Apps/Attendi/Views/ContentView.swift @@ -23,7 +23,7 @@ struct ContentView: View { var body: some View { NavigationStack { RecordingView( - recorder: model.recorder, + capturer: model.capturer, transcriber: model.transcriber, locale: $model.locale, ) { transcription in diff --git a/Packages/Features/Sources/Recording/Protocols/RecordingService.swift b/Packages/Features/Sources/Recording/Protocols/Capturing.swift similarity index 94% rename from Packages/Features/Sources/Recording/Protocols/RecordingService.swift rename to Packages/Features/Sources/Recording/Protocols/Capturing.swift index b2e6784..ec2526f 100644 --- a/Packages/Features/Sources/Recording/Protocols/RecordingService.swift +++ b/Packages/Features/Sources/Recording/Protocols/Capturing.swift @@ -4,7 +4,7 @@ import Foundation /// /// ``RecordingView`` attaches the service to its model at initialization, so the audio capture can be swapped without touching the feature's /// state machine — for example, with a real microphone backend in the app, or with a mock in unit tests. -public protocol RecordingService: Sendable { +public protocol Capturing: Sendable { // MARK: Methods diff --git a/Packages/Features/Sources/Recording/Services/AudioRecordingService.swift b/Packages/Features/Sources/Recording/Services/AudioCapturing.swift similarity index 80% rename from Packages/Features/Sources/Recording/Services/AudioRecordingService.swift rename to Packages/Features/Sources/Recording/Services/AudioCapturing.swift index 85fc417..0db91e5 100644 --- a/Packages/Features/Sources/Recording/Services/AudioRecordingService.swift +++ b/Packages/Features/Sources/Recording/Services/AudioCapturing.swift @@ -6,7 +6,7 @@ import AVFoundation /// The user's permission to record is requested before a recording starts and, on the platforms that require it, the shared audio session is /// configured for recording while the capture is in progress. @MainActor -public final class AudioRecordingService: RecordingService { +public final class AudioCapturing: Capturing { // MARK: Properties @@ -22,12 +22,12 @@ public final class AudioRecordingService: RecordingService { /// Starts a new audio recording from the microphone. /// - /// - Throws: ``AudioRecordingError/permissionNotGranted`` when the user denies the app access to the microphone, - /// ``AudioRecordingError/captureNotStarted`` when the recorder fails to start, or any error thrown while configuring + /// - Throws: ``AudioCapturingError/permissionNotGranted`` when the user denies the app access to the microphone, + /// ``AudioCapturingError/captureNotStarted`` when the recorder fails to start, or any error thrown while configuring /// the audio session or creating the recorder. public func start() async throws { guard await AVAudioApplication.requestRecordPermission() else { - throw AudioRecordingError.permissionNotGranted + throw AudioCapturingError.permissionNotGranted } #if os(iOS) || os(visionOS) @@ -43,7 +43,7 @@ public final class AudioRecordingService: RecordingService { ) guard recorder.record() else { - throw AudioRecordingError.captureNotStarted + throw AudioCapturingError.captureNotStarted } self.recorder = recorder @@ -51,10 +51,10 @@ public final class AudioRecordingService: RecordingService { /// Pauses the ongoing recording. /// - /// - Throws: ``AudioRecordingError/noOngoingRecording`` when no recording is in progress. + /// - Throws: ``AudioCapturingError/noOngoingRecording`` when no recording is in progress. public func pause() async throws { guard let recorder else { - throw AudioRecordingError.noOngoingRecording + throw AudioCapturingError.noOngoingRecording } recorder.pause() @@ -62,25 +62,25 @@ public final class AudioRecordingService: RecordingService { /// Resumes a paused recording. /// - /// - Throws: ``AudioRecordingError/noOngoingRecording`` when no recording is in progress, or - /// ``AudioRecordingError/captureNotStarted`` when the recorder fails to resume. + /// - Throws: ``AudioCapturingError/noOngoingRecording`` when no recording is in progress, or + /// ``AudioCapturingError/captureNotStarted`` when the recorder fails to resume. public func resume() async throws { guard let recorder else { - throw AudioRecordingError.noOngoingRecording + throw AudioCapturingError.noOngoingRecording } guard recorder.record() else { - throw AudioRecordingError.captureNotStarted + throw AudioCapturingError.captureNotStarted } } /// Stops the recording, deleting the temporary audio file after its contents are read. /// /// - Returns: The audio captured since the recording started. - /// - Throws: ``AudioRecordingError/noOngoingRecording`` when no recording is in progress, or any error thrown + /// - Throws: ``AudioCapturingError/noOngoingRecording`` when no recording is in progress, or any error thrown /// while reading the recorded audio file. public func stop() async throws -> Data { guard let recorder else { - throw AudioRecordingError.noOngoingRecording + throw AudioCapturingError.noOngoingRecording } recorder.stop() @@ -102,8 +102,8 @@ public final class AudioRecordingService: RecordingService { // MARK: - Errors -/// The errors thrown by ``AudioRecordingService``. -public enum AudioRecordingError: Error { +/// The errors thrown by ``AudioCapturing``. +public enum AudioCapturingError: Error { /// The recorder failed to start or resume the audio capture. case captureNotStarted /// No recording is in progress. diff --git a/Packages/Features/Sources/Recording/Services/DummyRecordingService.swift b/Packages/Features/Sources/Recording/Services/DummyCapturing.swift similarity index 93% rename from Packages/Features/Sources/Recording/Services/DummyRecordingService.swift rename to Packages/Features/Sources/Recording/Services/DummyCapturing.swift index 5c4f215..74b0e1c 100644 --- a/Packages/Features/Sources/Recording/Services/DummyRecordingService.swift +++ b/Packages/Features/Sources/Recording/Services/DummyCapturing.swift @@ -4,7 +4,7 @@ import Foundation /// /// The service is internal on purpose: it only backs the feature's previews and the default values of its model, and is not part of the /// package's public interface. -struct DummyRecordingService: RecordingService { +struct DummyCapturing: Capturing { // MARK: Methods diff --git a/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift b/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift index bc5c4cf..18ec58a 100644 --- a/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift +++ b/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift @@ -7,7 +7,7 @@ extension RecordingView { /// /// The model implements the recording flow as a ``State`` machine: it exposes the visibility and icon of the view's controls for the current state, counts the /// elapsed recording time, and processes the recorded input once it is sent. The audio capture and its transcription are delegated to the - /// ``RecordingService`` and ``Transcribing`` attached at initialization; when either of them fails, the model falls back to the not-recording state. + /// ``Capturing`` and ``Transcribing`` attached at initialization; when either of them fails, the model falls back to the not-recording state. @MainActor @Observable final class Model { @@ -41,7 +41,7 @@ extension RecordingView { /// The service that captures the audio from a microphone. @ObservationIgnored - private let recorder: any RecordingService + private let capturer: any Capturing /// The task that updates ``elapsedSeconds`` from the measured recording time once per second while recording. @ObservationIgnored @@ -56,15 +56,15 @@ extension RecordingView { /// Creates a model attached to the given recording and transcribing services. /// /// - Parameters: - /// - recorder: The service that captures the audio from a microphone. Defaults to ``DummyRecordingService``. + /// - capturer: The service that captures the audio from a microphone. Defaults to ``DummyCapturing``. /// - 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(), + capturer: any Capturing = DummyCapturing(), transcribe: any Transcribing = DummyTranscribing(), locale: Binding = .constant(.current) ) { - self.recorder = recorder + self.capturer = capturer self.transcribe = transcribe self.locale = locale } @@ -167,12 +167,12 @@ extension RecordingView { switch state { case .recording: startTimer(shouldRestartTimer) - startRecorder(shouldRestartTimer) + startCapturer(shouldRestartTimer) case .paused: stopTimer() Task { - try? await recorder.pause() + try? await capturer.pause() } case .processing: stopTimer() @@ -213,7 +213,7 @@ private extension RecordingView.Model { } do { - let audio = try await recorder.stop() + let audio = try await capturer.stop() transcription = try await transcribe( audio, @@ -228,17 +228,17 @@ private extension RecordingView.Model { state = .notRecording } - /// Starts the audio capture through the attached ``RecordingService``, 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 when the service fails. /// /// - Parameter isNewRecording: Whether a new recording should be started, as opposed to a paused one being resumed. - func startRecorder( + func startCapturer( _ isNewRecording: Bool ) { Task { do { isNewRecording - ? try await recorder.start() - : try await recorder.resume() + ? try await capturer.start() + : try await capturer.resume() } catch { stopTimer() diff --git a/Packages/Features/Sources/Recording/Views/RecordingView.swift b/Packages/Features/Sources/Recording/Views/RecordingView.swift index 5b90492..5eeba4d 100644 --- a/Packages/Features/Sources/Recording/Views/RecordingView.swift +++ b/Packages/Features/Sources/Recording/Views/RecordingView.swift @@ -21,18 +21,18 @@ public struct RecordingView: View { /// Creates a recording view in the not-recording state, attached to the given recording and transcribing services. /// /// - Parameters: - /// - recorder: The service that captures the audio from a microphone. + /// - capturer: The service that captures the audio from a microphone. /// - transcriber: The service that transcribes the recorded audio into text. /// - 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, + capturer: any Capturing, transcriber: any Transcribing, locale: Binding, onTranscription: @escaping (Transcription) -> Void ) { self.model = .init( - recorder: recorder, + capturer: capturer, transcribe: transcriber, locale: locale ) @@ -132,7 +132,7 @@ private enum Constant { "Recording view" ) { RecordingView( - recorder: DummyRecordingService(), + capturer: DummyCapturing(), transcriber: DummyTranscribing(), locale: .constant(.current) ) { _ in diff --git a/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift b/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift index 47f7263..e446e2b 100644 --- a/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift +++ b/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift @@ -249,31 +249,31 @@ struct RecordingViewModelTests { } - // MARK: Recorder + // MARK: Capturer @MainActor - @Suite("Recorder") - struct Recorder { + @Suite("Capturer") + struct Capturer { @Test func `starts the capture for a new recording`() async throws { - let recorder = RecordingServiceMock() - let model = Model(recorder: recorder) + let capturer = CapturingMock() + let model = Model(capturer: capturer) model.state = .recording model.updatedState(shouldRestartTimer: true) try await Task.sleep(for: .seconds(0.1)) - #expect(recorder.countStart == 1) - #expect(recorder.countResume == 0) + #expect(capturer.countStart == 1) + #expect(capturer.countResume == 0) model.state = .notRecording model.updatedState(shouldRestartTimer: false) } @Test func `pauses the capture while paused`() async throws { - let recorder = RecordingServiceMock() - let model = Model(recorder: recorder) + let capturer = CapturingMock() + let model = Model(capturer: capturer) model.state = .recording model.updatedState(shouldRestartTimer: true) @@ -283,12 +283,12 @@ struct RecordingViewModelTests { try await Task.sleep(for: .seconds(0.1)) - #expect(recorder.countPause == 1) + #expect(capturer.countPause == 1) } @Test func `resumes the capture after a pause`() async throws { - let recorder = RecordingServiceMock() - let model = Model(recorder: recorder) + let capturer = CapturingMock() + let model = Model(capturer: capturer) model.state = .recording model.updatedState(shouldRestartTimer: true) @@ -301,17 +301,17 @@ struct RecordingViewModelTests { try await Task.sleep(for: .seconds(0.1)) - #expect(recorder.countStart == 1) - #expect(recorder.countResume == 1) + #expect(capturer.countStart == 1) + #expect(capturer.countResume == 1) model.state = .notRecording model.updatedState(shouldRestartTimer: false) } @Test func `stops the capture when the input is sent`() async throws { - let recorder = RecordingServiceMock() + let capturer = CapturingMock() let model = Model( - recorder: recorder, + capturer: capturer, transcribe: TranscribingMock() ) @@ -320,15 +320,15 @@ struct RecordingViewModelTests { try await Task.sleep(for: .seconds(0.5)) - #expect(recorder.countStop == 1) + #expect(capturer.countStop == 1) } @Test func `falls back to not recording when the capture fails`() async throws { - let recorder = RecordingServiceMock() + let capturer = CapturingMock() - recorder.error = ErrorMock() + capturer.error = ErrorMock() - let model = Model(recorder: recorder) + let model = Model(capturer: capturer) model.state = .recording model.updatedState(shouldRestartTimer: true) @@ -419,7 +419,7 @@ struct RecordingViewModelTests { /// A recording service that counts its invocations and can be configured to fail. @MainActor -private final class RecordingServiceMock: RecordingService { +private final class CapturingMock: Capturing { /// The error the service throws from every method, or `nil` when it should succeed. var error: Error?