Updated the README file of the project.

This commit is contained in:
2026-07-04 16:00:13 +02:00
parent b0c6e25639
commit beb4f7b4cc
+84 -41
View File
@@ -1,7 +1,8 @@
# Attendi Sample App # Attendi Sample App
A sample app that implements an audio recording flow in SwiftUI: start, pause, and resume a recording while a timer A sample app that implements an audio recording flow in SwiftUI: start, pause, resume, or discard a recording while a
tracks the elapsed time, then send the recording for (simulated) processing. timer tracks the elapsed time, then send the recording to be transcribed on device — and read the resulting text in a
sheet.
## Context ## 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 [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). healthcare professionals, based on [a given set of requirements](README.pdf).
## Demo
[▶️ Watch the sample (iOS) app in action!](Attendi_Sample_App.MP4)
## Requirements ## Requirements
| Requirement | Version | | Requirement | Version |
@@ -29,24 +26,26 @@ The project separates the app shell from the feature code, which lives in a loca
Attendi/ Attendi/
├── Attendi.xcodeproj ├── Attendi.xcodeproj
├── Apps/ ├── Apps/
│ └── Attendi/ # The app target (thin shell) │ └── Attendi/ # The app target (thin shell)
│ ├── AttendiApp.swift # The @main entry point │ ├── App/ # The @main entry point
│ ├── ContentView.swift # The root view, hosting the Recording feature │ ├── Views/ # The root view: hosts the feature, the locale picker, and the transcription sheet
── Assets.xcassets ── 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/ └── Packages/
└── Features/ # Local Swift package with the feature code └── Features/ # Local Swift package with the feature code
├── Package.swift ├── Package.swift
├── Sources/ ├── Sources/
│ └── Recording/ # The Recording feature target │ └── Recording/ # The Recording feature target
│ ├── RecordingView.swift │ ├── Catalogs/ # The record, pause, and send icons; the localized strings
│ ├── RecordingViewModel.swift │ ├── Models/ # Transcription, RecordingError
│ ├── RecordingButtonStyle.swift │ ├── Protocols/ # Capturing, Transcribing
│ ├── RecordingService.swift │ ├── Services/ # AudioCapturing, AudioTranscribing, and their development dummies
│ ├── TranscribingService.swift │ ├── Styles/ # RecordingButtonStyle
── Images.xcassets # Record, pause, and send icons ── View Models/ # RecordingViewModel
│ └── Views/ # RecordingView
└── Tests/ └── Tests/
├── Recording/ ├── Recording/ # The model and view model suites, with their mocks
│ └── RecordingViewModelTests.swift
└── Features.xctestplan └── 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. | | `notRecording` | Idle; no recording in progress. |
| `recording` | A recording is in progress and the timer is ticking. | | `recording` | A recording is in progress and the timer is ticking. |
| `paused` | The recording is paused; it can be resumed or sent. | | `paused` | The recording is paused; it can be resumed, discarded, or sent. |
| `processing` | The sent recording is being processed. | | `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 The main button starts, pauses, and resumes a recording. While paused, two more buttons appear: discard throws the
flow into processing (currently simulated with a delay) before returning to idle. 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 ### Timer
While recording, an async task increments the elapsed seconds once per second — pausing stops it, resuming continues The recording time is measured with a monotonic `ContinuousClock`: an anchor marks the start of the current recording
it, and a new recording resets it. The view formats the count as `mm:ss` and animates digit changes with a numeric stretch, and pausing folds the stretch into an accumulated duration, so scheduling latency never accumulates as drift.
text content transition. 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 ### Views
- **`RecordingView`** — the public entry point of the feature. It renders the timer label and the control buttons for - **`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 - **`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 Liquid Glass background that reacts fluidly to presses, and dims while disabled. The label and its padding scale
via `@ScaledMetric`, and an `invertStyle` flag controls whether the label's color scheme is inverted for contrast. with Dynamic Type via `@ScaledMetric`, and an `invertStyle` flag controls whether the label's color scheme is
inverted for contrast.
### Services ### Services
The actual recording work is abstracted behind two protocols, injected into `RecordingView` at initialization and The actual recording work is abstracted behind two protocols, injected into `RecordingView` at initialization and
attached to its view model: attached to its view model:
- **`RecordingService`** — captures the audio from a microphone, with throwing async `start`, `pause`, `resume`, and - **`Capturing`** — captures the audio from a microphone, with throwing async `start`, `pause`, `resume`, and `stop`
`stop` methods; `stop` returns the captured audio. methods; `stop` hands the captured audio over as a file URL, avoiding any in-memory copies. An `events` stream
- **`TranscribingService`** — transcribes the captured audio into text through a throwing async `transcribe` method; (empty by default) lets a service report interruptions of the capture.
the view model stores the result once processing finishes. - **`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 The real backends used by the app are:
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 - **`AudioCapturing`** — records single-channel AAC into a temporary file through `AVAudioRecorder`, requesting the
not-recording state. 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 ## Testing
The view model is covered by Swift Testing suites in `RecordingViewModelTests.swift`, grouped with nested `@Suite` The package is covered by Swift Testing suites: `RecordingViewModelTests.swift` groups the view model's behaviors with
types (initial state, button presses, computed properties, timer, and processing). The state-dependent behaviors are nested `@Suite` types (initial state, button presses, computed properties, timer, capturer, errors, interruptions, and
exercised with parameterized tests across all four states. 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: 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' 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 ## Tooling
- **`.swift-format`** — configuration for Apple's `swift-format`, used by Xcode's built-in formatter (4-space - **`.swift-format`** — configuration for Apple's `swift-format`, used by Xcode's built-in formatter (4-space