From beb4f7b4cca7a25fcf67b6cdc263ff6d6c6b3f31 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 4 Jul 2026 16:00:13 +0200 Subject: [PATCH] Updated the README file of the project. --- README.md | 125 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 84 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 053b00f..be9b0e7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Attendi Sample App -A sample app that implements an audio recording flow in SwiftUI: start, pause, and resume a recording while a timer -tracks the elapsed time, then send the recording for (simulated) processing. +A sample app that implements an audio recording flow in SwiftUI: start, pause, resume, or discard a recording while a +timer tracks the elapsed time, then send the recording to be transcribed on device — and read the resulting text in a +sheet. ## Context @@ -9,10 +10,6 @@ This sample app was created for a technical interview for a senior Mobile (iOS + [Attendi](https://attendi.nl), a Dutch health-tech company that makes voice-driven reporting software for healthcare professionals, based on [a given set of requirements](README.pdf). -## Demo - -[▶️ Watch the sample (iOS) app in action!](Attendi_Sample_App.MP4) - ## Requirements | Requirement | Version | @@ -29,24 +26,26 @@ The project separates the app shell from the feature code, which lives in a loca Attendi/ ├── Attendi.xcodeproj ├── Apps/ -│ └── Attendi/ # The app target (thin shell) -│ ├── AttendiApp.swift # The @main entry point -│ ├── ContentView.swift # The root view, hosting the Recording feature -│ └── Assets.xcassets +│ └── Attendi/ # The app target (thin shell) +│ ├── App/ # The @main entry point +│ ├── Views/ # The root view: hosts the feature, the locale picker, and the transcription sheet +│ ├── View Models/ # The services, the supported locales, and the speech model preinstallation +│ ├── Catalogs/ # The app's assets and localized strings +│ └── Resources/ # The Info.plist, including the microphone usage description └── Packages/ - └── Features/ # Local Swift package with the feature code + └── Features/ # Local Swift package with the feature code ├── Package.swift ├── Sources/ - │ └── Recording/ # The Recording feature target - │ ├── RecordingView.swift - │ ├── RecordingViewModel.swift - │ ├── RecordingButtonStyle.swift - │ ├── RecordingService.swift - │ ├── TranscribingService.swift - │ └── Images.xcassets # Record, pause, and send icons + │ └── Recording/ # The Recording feature target + │ ├── Catalogs/ # The record, pause, and send icons; the localized strings + │ ├── Models/ # Transcription, RecordingError + │ ├── Protocols/ # Capturing, Transcribing + │ ├── Services/ # AudioCapturing, AudioTranscribing, and their development dummies + │ ├── Styles/ # RecordingButtonStyle + │ ├── View Models/ # RecordingViewModel + │ └── Views/ # RecordingView └── Tests/ - ├── Recording/ - │ └── RecordingViewModelTests.swift + ├── Recording/ # The model and view model suites, with their mocks └── Features.xctestplan ``` @@ -63,46 +62,84 @@ The flow is modeled as a state machine in `RecordingView.Model`, an `@Observable | --- | --- | | `notRecording` | Idle; no recording in progress. | | `recording` | A recording is in progress and the timer is ticking. | -| `paused` | The recording is paused; it can be resumed or sent. | -| `processing` | The sent recording is being processed. | +| `paused` | The recording is paused; it can be resumed, discarded, or sent. | +| `processing` | The sent recording is being stopped and transcribed. | -The main button starts, pauses, and resumes a recording. While paused, a send button appears; pressing it moves the -flow into processing (currently simulated with a delay) before returning to idle. +The main button starts, pauses, and resumes a recording. While paused, two more buttons appear: discard throws the +recording away, and send moves the flow into processing — stopping the capture and transcribing the audio — before +returning to idle. The model owns every side effect of a transition (timer, capture, processing), so the state machine +works the same whether it is driven by the view's buttons or by the unit tests. + +The calls to the capturing service are serialized through a task chain: no matter how quickly the states change, +`start`, `pause`, `resume`, and `stop` always reach the service in order. When the system interrupts the audio session +mid-recording — a phone call or Siri, for example — the service emits an event and the model pauses exactly like a +press of the main button would; the capture is never resumed without the user asking for it. ### Timer -While recording, an async task increments the elapsed seconds once per second — pausing stops it, resuming continues -it, and a new recording resets it. The view formats the count as `mm:ss` and animates digit changes with a numeric -text content transition. +The recording time is measured with a monotonic `ContinuousClock`: an anchor marks the start of the current recording +stretch, and pausing folds the stretch into an accumulated duration, so scheduling latency never accumulates as drift. +While recording, an async task republishes the measured time once per second. The view formats the count as `mm:ss` +and animates digit changes with a numeric text content transition. + +### Errors + +Failures of either service are published by the model as a `RecordingError` and presented by the view in an alert: a +denied microphone permission additionally offers to open the app's settings, and unavailable speech model assets are +distinguished from an ordinary transcription failure. The underlying errors are logged through `OSLog` under the +`Features.Recording` subsystem. ### Views - **`RecordingView`** — the public entry point of the feature. It renders the timer label and the control buttons for - the model's current state, and forwards state changes back to the model. + the model's current state, forwards every transcription to a closure given at initialization, and presents the + error alert. All controls carry localized accessibility labels matching their current action, and the timer + exposes its value to assistive technologies spelled out in full units. - **`RecordingButtonStyle`** — a custom `ButtonStyle` used by the controls: the label sits on a padded, circular red - background, shrinks while pressed, and dims while disabled. The label and its padding scale with Dynamic Type - via `@ScaledMetric`, and an `invertStyle` flag controls whether the label's color scheme is inverted for contrast. + Liquid Glass background that reacts fluidly to presses, and dims while disabled. The label and its padding scale + with Dynamic Type via `@ScaledMetric`, and an `invertStyle` flag controls whether the label's color scheme is + inverted for contrast. ### Services The actual recording work is abstracted behind two protocols, injected into `RecordingView` at initialization and attached to its view model: -- **`RecordingService`** — captures the audio from a microphone, with throwing async `start`, `pause`, `resume`, and - `stop` methods; `stop` returns the captured audio. -- **`TranscribingService`** — transcribes the captured audio into text through a throwing async `transcribe` method; - the view model stores the result once processing finishes. +- **`Capturing`** — captures the audio from a microphone, with throwing async `start`, `pause`, `resume`, and `stop` + methods; `stop` hands the captured audio over as a file URL, avoiding any in-memory copies. An `events` stream + (empty by default) lets a service report interruptions of the capture. +- **`Transcribing`** — a callable service that transcribes a recorded audio file into text for a given locale, + returning a `Transcription`. -The default `SimulatedRecordingService` and `SimulatedTranscribingService` fake the work (the latter with a two-second -delay and a dummy transcription); the unit tests inject fast mocks, and real backends can be plugged in the same way -without touching the feature's state machine. When either service fails, the view model falls back to the -not-recording state. +The real backends used by the app are: + +- **`AudioCapturing`** — records single-channel AAC into a temporary file through `AVAudioRecorder`, requesting the + microphone permission and managing the shared audio session on the platforms that require it, and emitting an + event when the system interrupts the session while recording. +- **`AudioTranscribing`** — transcribes entirely on device through `SpeechAnalyzer` with a `SpeechTranscriber` module, + in the closest supported equivalent of the requested locale, installing any missing speech model assets before + the analysis. + +The internal `DummyCapturing` and `DummyTranscribing` fake the work for previews and default model values (the latter +with a two-second delay and a dummy transcription); the unit tests inject configurable mocks, and other backends can +be plugged in the same way without touching the feature's state machine. + +## The app shell + +`ContentView` hosts the feature inside a navigation stack and presents the transcribed text of every processed +recording in a modal sheet — or a content-unavailable message when the transcription is empty. A toolbar menu picks +the locale of the spoken language from the locales the transcriber supports, aligned at launch with the closest +supported equivalent of the user's locale. Picking a locale preinstalls its speech model assets in the background, so +the first transcription does not have to download them mid-processing; the app keeps a single asset locale reservation +at a time, releasing stale ones, since the system only permits a limited number per app. ## Testing -The view model is covered by Swift Testing suites in `RecordingViewModelTests.swift`, grouped with nested `@Suite` -types (initial state, button presses, computed properties, timer, and processing). The state-dependent behaviors are -exercised with parameterized tests across all four states. +The package is covered by Swift Testing suites: `RecordingViewModelTests.swift` groups the view model's behaviors with +nested `@Suite` types (initial state, button presses, computed properties, timer, capturer, errors, interruptions, and +processing), and `TranscriptionTests.swift` covers the `Transcription` model. The state-dependent behaviors are +exercised with parameterized tests across all four states, driven through the model's public press handlers, and the +service mocks record their invocations in call order. Run the tests with the `Features` scheme in Xcode (`⌘U`), or from the command line: @@ -110,6 +147,12 @@ Run the tests with the `Features` scheme in Xcode (`⌘U`), or from the command xcodebuild test -scheme Features -destination 'platform=iOS' ``` +## Localization + +Both the app and the feature package localize their user-facing text through string catalogs (`Localizable.xcstrings`, +currently English only): the app's catalog covers the navigation titles and the transcription sheet, and the package's +catalog covers the error alert and the accessibility labels. + ## Tooling - **`.swift-format`** — configuration for Apple's `swift-format`, used by Xcode's built-in formatter (4-space