From aa9ec016115595a3ed3053b22d1ee72393281468 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 3 Jul 2026 23:57:49 +0200 Subject: [PATCH] Reorganized the Recording package target into folders. --- Attendi.xcodeproj/project.pbxproj | 2 - .../Images.xcassets/Contents.json | 0 .../Images.xcassets/Icon/Contents.json | 0 .../Icon/Pause.imageset/Contents.json | 0 .../Icon/Pause.imageset/pause.svg | 0 .../Icon/Record.imageset/Contents.json | 0 .../Icon/Record.imageset/microphone.svg | 0 .../Icon/Send.imageset/Contents.json | 0 .../Icon/Send.imageset/send.svg | 0 .../{ => Protocols}/RecordingService.swift | 30 ---------- .../Protocols/TranscribingService.swift | 15 +++++ .../Services}/AudioRecordingService.swift | 18 +++--- .../Services}/AudioTranscribingService.swift | 17 +++--- .../Services/DummyRecordingService.swift | 27 +++++++++ .../Services/DummyTranscribingService.swift | 38 +++++++++++++ .../Buttons}/RecordingButtonStyle.swift | 0 .../Recording/TranscribingService.swift | 56 ------------------- .../RecordingViewModel.swift | 8 +-- .../Recording/{ => Views}/RecordingView.swift | 4 +- 19 files changed, 103 insertions(+), 112 deletions(-) rename Packages/Features/Sources/Recording/{ => Catalogs}/Images.xcassets/Contents.json (100%) rename Packages/Features/Sources/Recording/{ => Catalogs}/Images.xcassets/Icon/Contents.json (100%) rename Packages/Features/Sources/Recording/{ => Catalogs}/Images.xcassets/Icon/Pause.imageset/Contents.json (100%) rename Packages/Features/Sources/Recording/{ => Catalogs}/Images.xcassets/Icon/Pause.imageset/pause.svg (100%) rename Packages/Features/Sources/Recording/{ => Catalogs}/Images.xcassets/Icon/Record.imageset/Contents.json (100%) rename Packages/Features/Sources/Recording/{ => Catalogs}/Images.xcassets/Icon/Record.imageset/microphone.svg (100%) rename Packages/Features/Sources/Recording/{ => Catalogs}/Images.xcassets/Icon/Send.imageset/Contents.json (100%) rename Packages/Features/Sources/Recording/{ => Catalogs}/Images.xcassets/Icon/Send.imageset/send.svg (100%) rename Packages/Features/Sources/Recording/{ => Protocols}/RecordingService.swift (50%) create mode 100644 Packages/Features/Sources/Recording/Protocols/TranscribingService.swift rename {Apps/Attendi => Packages/Features/Sources/Recording/Services}/AudioRecordingService.swift (91%) rename {Apps/Attendi => Packages/Features/Sources/Recording/Services}/AudioTranscribingService.swift (84%) create mode 100644 Packages/Features/Sources/Recording/Services/DummyRecordingService.swift create mode 100644 Packages/Features/Sources/Recording/Services/DummyTranscribingService.swift rename Packages/Features/Sources/Recording/{ => Styles/Buttons}/RecordingButtonStyle.swift (100%) delete mode 100644 Packages/Features/Sources/Recording/TranscribingService.swift rename Packages/Features/Sources/Recording/{ => View Models}/RecordingViewModel.swift (96%) rename Packages/Features/Sources/Recording/{ => Views}/RecordingView.swift (97%) diff --git a/Attendi.xcodeproj/project.pbxproj b/Attendi.xcodeproj/project.pbxproj index c1ada36..a08dd89 100644 --- a/Attendi.xcodeproj/project.pbxproj +++ b/Attendi.xcodeproj/project.pbxproj @@ -23,8 +23,6 @@ membershipExceptions = ( Attendi/Assets.xcassets, Attendi/AttendiApp.swift, - Attendi/AudioRecordingService.swift, - Attendi/AudioTranscribingService.swift, Attendi/ContentView.swift, ); target = 02870A0C2FF7EB610079EA3A /* Attendi */; diff --git a/Packages/Features/Sources/Recording/Images.xcassets/Contents.json b/Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Contents.json similarity index 100% rename from Packages/Features/Sources/Recording/Images.xcassets/Contents.json rename to Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Contents.json diff --git a/Packages/Features/Sources/Recording/Images.xcassets/Icon/Contents.json b/Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Contents.json similarity index 100% rename from Packages/Features/Sources/Recording/Images.xcassets/Icon/Contents.json rename to Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Contents.json diff --git a/Packages/Features/Sources/Recording/Images.xcassets/Icon/Pause.imageset/Contents.json b/Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Pause.imageset/Contents.json similarity index 100% rename from Packages/Features/Sources/Recording/Images.xcassets/Icon/Pause.imageset/Contents.json rename to Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Pause.imageset/Contents.json diff --git a/Packages/Features/Sources/Recording/Images.xcassets/Icon/Pause.imageset/pause.svg b/Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Pause.imageset/pause.svg similarity index 100% rename from Packages/Features/Sources/Recording/Images.xcassets/Icon/Pause.imageset/pause.svg rename to Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Pause.imageset/pause.svg diff --git a/Packages/Features/Sources/Recording/Images.xcassets/Icon/Record.imageset/Contents.json b/Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Record.imageset/Contents.json similarity index 100% rename from Packages/Features/Sources/Recording/Images.xcassets/Icon/Record.imageset/Contents.json rename to Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Record.imageset/Contents.json diff --git a/Packages/Features/Sources/Recording/Images.xcassets/Icon/Record.imageset/microphone.svg b/Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Record.imageset/microphone.svg similarity index 100% rename from Packages/Features/Sources/Recording/Images.xcassets/Icon/Record.imageset/microphone.svg rename to Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Record.imageset/microphone.svg diff --git a/Packages/Features/Sources/Recording/Images.xcassets/Icon/Send.imageset/Contents.json b/Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Send.imageset/Contents.json similarity index 100% rename from Packages/Features/Sources/Recording/Images.xcassets/Icon/Send.imageset/Contents.json rename to Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Send.imageset/Contents.json diff --git a/Packages/Features/Sources/Recording/Images.xcassets/Icon/Send.imageset/send.svg b/Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Send.imageset/send.svg similarity index 100% rename from Packages/Features/Sources/Recording/Images.xcassets/Icon/Send.imageset/send.svg rename to Packages/Features/Sources/Recording/Catalogs/Images.xcassets/Icon/Send.imageset/send.svg diff --git a/Packages/Features/Sources/Recording/RecordingService.swift b/Packages/Features/Sources/Recording/Protocols/RecordingService.swift similarity index 50% rename from Packages/Features/Sources/Recording/RecordingService.swift rename to Packages/Features/Sources/Recording/Protocols/RecordingService.swift index 8db1ab8..95253c8 100644 --- a/Packages/Features/Sources/Recording/RecordingService.swift +++ b/Packages/Features/Sources/Recording/Protocols/RecordingService.swift @@ -21,33 +21,3 @@ public protocol RecordingService: Sendable { func stop() async throws -> Data } - -// MARK: - Services - -/// The recording service used by default, which simulates the audio capture without touching a microphone. -public struct SimulatedRecordingService: RecordingService { - - // MARK: Initializers - - /// Creates a simulated recording service. - public init() {} - - // MARK: Methods - - /// Simulates the start of an audio recording. - public func start() async throws {} - - /// Simulates the pause of an ongoing recording. - public func pause() async throws {} - - /// Simulates the resumption of a paused recording. - public func resume() async throws {} - - /// Simulates the stop of a recording. - /// - /// - Returns: An empty audio payload. - public func stop() async throws -> Data { - .init() - } - -} diff --git a/Packages/Features/Sources/Recording/Protocols/TranscribingService.swift b/Packages/Features/Sources/Recording/Protocols/TranscribingService.swift new file mode 100644 index 0000000..3687df6 --- /dev/null +++ b/Packages/Features/Sources/Recording/Protocols/TranscribingService.swift @@ -0,0 +1,15 @@ +import Foundation + +/// A service that transcribes recorded audio into text for the Recording feature. +/// +/// ``RecordingView`` attaches the service to its model at initialization, so the transcription can be swapped without touching the feature's +/// state machine — for example, with a real transcription backend in the app, or with a fast mock in unit tests. +public protocol TranscribingService: Sendable { + + /// Transcribes the given recorded audio into text. + /// + /// - Parameter audio: The recorded audio to transcribe. + /// - Returns: The transcription of the recorded audio. + func transcribe(_ audio: Data) async throws -> String + +} diff --git a/Apps/Attendi/AudioRecordingService.swift b/Packages/Features/Sources/Recording/Services/AudioRecordingService.swift similarity index 91% rename from Apps/Attendi/AudioRecordingService.swift rename to Packages/Features/Sources/Recording/Services/AudioRecordingService.swift index a4f9d43..85fc417 100644 --- a/Apps/Attendi/AudioRecordingService.swift +++ b/Packages/Features/Sources/Recording/Services/AudioRecordingService.swift @@ -1,13 +1,12 @@ import AVFoundation -import Recording -/// The recording service used by the Attendi sample app, which captures real audio from the device's microphone. +/// The recording service that captures real audio from the device's microphone. /// /// The service records into a temporary `.m4a` file through an `AVAudioRecorder`, and returns the file's contents when the recording stops. /// 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 -final class AudioRecordingService: RecordingService { +public final class AudioRecordingService: RecordingService { // MARK: Properties @@ -17,7 +16,7 @@ final class AudioRecordingService: RecordingService { // MARK: Initializers /// Creates an audio recording service. - init() {} + public init() {} // MARK: Methods @@ -26,7 +25,7 @@ final class AudioRecordingService: RecordingService { /// - 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 /// the audio session or creating the recorder. - func start() async throws { + public func start() async throws { guard await AVAudioApplication.requestRecordPermission() else { throw AudioRecordingError.permissionNotGranted } @@ -53,7 +52,7 @@ final class AudioRecordingService: RecordingService { /// Pauses the ongoing recording. /// /// - Throws: ``AudioRecordingError/noOngoingRecording`` when no recording is in progress. - func pause() async throws { + public func pause() async throws { guard let recorder else { throw AudioRecordingError.noOngoingRecording } @@ -65,7 +64,7 @@ final class AudioRecordingService: RecordingService { /// /// - Throws: ``AudioRecordingError/noOngoingRecording`` when no recording is in progress, or /// ``AudioRecordingError/captureNotStarted`` when the recorder fails to resume. - func resume() async throws { + public func resume() async throws { guard let recorder else { throw AudioRecordingError.noOngoingRecording } @@ -79,7 +78,7 @@ final class AudioRecordingService: RecordingService { /// - Returns: The audio captured since the recording started. /// - Throws: ``AudioRecordingError/noOngoingRecording`` when no recording is in progress, or any error thrown /// while reading the recorded audio file. - func stop() async throws -> Data { + public func stop() async throws -> Data { guard let recorder else { throw AudioRecordingError.noOngoingRecording } @@ -104,7 +103,7 @@ final class AudioRecordingService: RecordingService { // MARK: - Errors /// The errors thrown by ``AudioRecordingService``. -enum AudioRecordingError: Error { +public enum AudioRecordingError: Error { /// The recorder failed to start or resume the audio capture. case captureNotStarted /// No recording is in progress. @@ -120,6 +119,7 @@ private enum Constant { /// The audio constants. enum Audio { /// The settings of the recorded audio: single-channel AAC at a 44.1 kHz sample rate, encoded in high quality. + @MainActor static let settings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVNumberOfChannelsKey: 1, diff --git a/Apps/Attendi/AudioTranscribingService.swift b/Packages/Features/Sources/Recording/Services/AudioTranscribingService.swift similarity index 84% rename from Apps/Attendi/AudioTranscribingService.swift rename to Packages/Features/Sources/Recording/Services/AudioTranscribingService.swift index bed14c2..f24f62b 100644 --- a/Apps/Attendi/AudioTranscribingService.swift +++ b/Packages/Features/Sources/Recording/Services/AudioTranscribingService.swift @@ -1,18 +1,17 @@ import AVFoundation -import Recording import Speech -/// The transcribing service used by the Attendi sample app, which transcribes recorded audio into text on device. +/// The transcribing service that transcribes recorded audio into text on device. /// /// The service writes the recorded audio into a temporary `.m4a` file and runs it through a `SpeechAnalyzer` with a `SpeechTranscriber` /// module, joining the finalized results into the returned text. The speech model assets for the current locale are downloaded and installed /// on first use; every transcription after that happens entirely offline. -struct AudioTranscribingService: TranscribingService { +public struct AudioTranscribingService: TranscribingService { // MARK: Initializers - /// Creates a speech transcribing service. - init() {} + /// Creates an audio transcribing service. + public init() {} // MARK: Methods @@ -22,7 +21,7 @@ struct AudioTranscribingService: TranscribingService { /// - Returns: The transcription of the recorded audio. /// - Throws: ``AudioTranscribingError/localeNotSupported`` when the transcriber does not support the current locale, or any /// error thrown while installing the speech model assets, reading the audio file, or analyzing its contents. - func transcribe(_ audio: Data) async throws -> String { + public func transcribe(_ audio: Data) async throws -> String { try audio.write(to: Constant.File.url) defer { @@ -64,15 +63,15 @@ struct AudioTranscribingService: TranscribingService { // MARK: - Errors /// The errors thrown by ``AudioTranscribingService``. -enum AudioTranscribingError: Error { +public enum AudioTranscribingError: Error { /// The transcriber does not support the current locale. case localeNotSupported } // MARK: - Constants -/// The constant values used across the speech transcribing service. -private nonisolated enum Constant { +/// The constant values used across the audio transcribing service. +private enum Constant { /// The file constants. enum File { /// The location of the temporary file the recorded audio is written into for the analysis. diff --git a/Packages/Features/Sources/Recording/Services/DummyRecordingService.swift b/Packages/Features/Sources/Recording/Services/DummyRecordingService.swift new file mode 100644 index 0000000..5c4f215 --- /dev/null +++ b/Packages/Features/Sources/Recording/Services/DummyRecordingService.swift @@ -0,0 +1,27 @@ +import Foundation + +/// The recording service used for development, which simulates the audio capture without touching a microphone. +/// +/// 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 { + + // MARK: Methods + + /// Simulates the start of an audio recording. + func start() async throws {} + + /// Simulates the pause of an ongoing recording. + func pause() async throws {} + + /// Simulates the resumption of a paused recording. + func resume() async throws {} + + /// Simulates the stop of a recording. + /// + /// - Returns: An empty audio payload. + func stop() async throws -> Data { + .init() + } + +} diff --git a/Packages/Features/Sources/Recording/Services/DummyTranscribingService.swift b/Packages/Features/Sources/Recording/Services/DummyTranscribingService.swift new file mode 100644 index 0000000..bdff9a9 --- /dev/null +++ b/Packages/Features/Sources/Recording/Services/DummyTranscribingService.swift @@ -0,0 +1,38 @@ +import Foundation + +/// The transcribing service used for development, which simulates the transcription work. +/// +/// 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 DummyTranscribingService: TranscribingService { + + // MARK: Methods + + /// Simulates the transcription of the given recorded audio with a two-second delay. + /// + /// - Parameter audio: The recorded audio to transcribe. + /// - Returns: A dummy transcription. + func transcribe(_ audio: Data) async throws -> String { + try await Task.sleep(for: Constant.Delay.transcribing) + + return Constant.Text.transcription + } + +} + +// MARK: - Constants + +/// The constant values used across the transcribing services. +private enum Constant { + /// The delay constants. + enum Delay { + /// The duration of the simulated transcription work. + static let transcribing: Duration = .seconds(2) + } + + /// The text constants. + enum Text { + /// The dummy transcription of a recorded audio. + static let transcription = "This is a dummy transcription of the recorded audio." + } +} diff --git a/Packages/Features/Sources/Recording/RecordingButtonStyle.swift b/Packages/Features/Sources/Recording/Styles/Buttons/RecordingButtonStyle.swift similarity index 100% rename from Packages/Features/Sources/Recording/RecordingButtonStyle.swift rename to Packages/Features/Sources/Recording/Styles/Buttons/RecordingButtonStyle.swift diff --git a/Packages/Features/Sources/Recording/TranscribingService.swift b/Packages/Features/Sources/Recording/TranscribingService.swift deleted file mode 100644 index d71ef7e..0000000 --- a/Packages/Features/Sources/Recording/TranscribingService.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Foundation - -/// A service that transcribes recorded audio into text for the Recording feature. -/// -/// ``RecordingView`` attaches the service to its model at initialization, so the transcription can be swapped without touching the feature's -/// state machine — for example, with a real transcription backend in the app, or with a fast mock in unit tests. -public protocol TranscribingService: Sendable { - - /// Transcribes the given recorded audio into text. - /// - /// - Parameter audio: The recorded audio to transcribe. - /// - Returns: The transcription of the recorded audio. - func transcribe(_ audio: Data) async throws -> String - -} - -// MARK: - Services - -/// The transcribing service used by default, which simulates the transcription work. -public struct SimulatedTranscribingService: TranscribingService { - - // MARK: Initializers - - /// Creates a simulated transcribing service. - public init() {} - - // MARK: Methods - - /// Simulates the transcription of the given recorded audio with a two-second delay. - /// - /// - Parameter audio: The recorded audio to transcribe. - /// - Returns: A dummy transcription. - public func transcribe(_ audio: Data) async throws -> String { - try await Task.sleep(for: Constant.Delay.transcribing) - - return Constant.Text.transcription - } - -} - -// MARK: - Constants - -/// The constant values used across the transcribing services. -private enum Constant { - /// The delay constants. - enum Delay { - /// The duration of the simulated transcription work. - static let transcribing: Duration = .seconds(2) - } - - /// The text constants. - enum Text { - /// The dummy transcription of a recorded audio. - static let transcription = "This is a dummy transcription of the recorded audio." - } -} diff --git a/Packages/Features/Sources/Recording/RecordingViewModel.swift b/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift similarity index 96% rename from Packages/Features/Sources/Recording/RecordingViewModel.swift rename to Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift index 3a90a1e..6a12de8 100644 --- a/Packages/Features/Sources/Recording/RecordingViewModel.swift +++ b/Packages/Features/Sources/Recording/View Models/RecordingViewModel.swift @@ -40,11 +40,11 @@ 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 ``SimulatedRecordingService``. - /// - transcriber: The service that transcribes the recorded audio into text. Defaults to ``SimulatedTranscribingService``. + /// - recorder: The service that captures the audio from a microphone. Defaults to ``DummyRecordingService``. + /// - transcriber: The service that transcribes the recorded audio into text. Defaults to ``DummyTranscribingService``. init( - recorder: any RecordingService = SimulatedRecordingService(), - transcriber: any TranscribingService = SimulatedTranscribingService() + recorder: any RecordingService = DummyRecordingService(), + transcriber: any TranscribingService = DummyTranscribingService() ) { self.recorder = recorder self.transcriber = transcriber diff --git a/Packages/Features/Sources/Recording/RecordingView.swift b/Packages/Features/Sources/Recording/Views/RecordingView.swift similarity index 97% rename from Packages/Features/Sources/Recording/RecordingView.swift rename to Packages/Features/Sources/Recording/Views/RecordingView.swift index 29f0bb4..6c1eeac 100644 --- a/Packages/Features/Sources/Recording/RecordingView.swift +++ b/Packages/Features/Sources/Recording/Views/RecordingView.swift @@ -123,7 +123,7 @@ private enum Constant { "Recording view" ) { RecordingView( - recorder: SimulatedRecordingService(), - transcriber: SimulatedTranscribingService() + recorder: DummyRecordingService(), + transcriber: DummyTranscribingService() ) }