160 lines
8.9 KiB
Markdown
160 lines
8.9 KiB
Markdown
# Attendi Sample App
|
|
|
|
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
|
|
|
|
This sample app was created for a technical interview for a senior Mobile (iOS + Android) engineer role at
|
|
[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).
|
|
|
|
## Requirements
|
|
|
|
| Requirement | Version |
|
|
| --- | --- |
|
|
| Xcode | 26 or later |
|
|
| Swift | 6.3 |
|
|
| Platforms | iOS/macOS/visionOS 26 |
|
|
|
|
## Project structure
|
|
|
|
The project separates the app shell from the feature code, which lives in a local Swift package:
|
|
|
|
```
|
|
Attendi/
|
|
├── Attendi.xcodeproj
|
|
├── Apps/
|
|
│ └── 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
|
|
├── Package.swift
|
|
├── Sources/
|
|
│ └── 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/ # The model and view model suites, with their mocks
|
|
└── Features.xctestplan
|
|
```
|
|
|
|
The app target only depends on the `Features` package and renders its public `RecordingView` view; all recording logic
|
|
and UI live in the package's `Recording` target.
|
|
|
|
## The Recording feature
|
|
|
|
### State machine
|
|
|
|
The flow is modeled as a state machine in `RecordingView.Model`, an `@Observable`, `@MainActor` view model:
|
|
|
|
| State | Meaning |
|
|
| --- | --- |
|
|
| `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, discarded, or sent. |
|
|
| `processing` | The sent recording is being stopped and transcribed. |
|
|
|
|
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
|
|
|
|
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, 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
|
|
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:
|
|
|
|
- **`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 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 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:
|
|
|
|
```sh
|
|
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
|
|
indentation, 200-column lines).
|