Refreshed the documentation of the README file in the project one last time.
This commit is contained in:
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 7.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.5 MiB |
Binary file not shown.
@@ -2,16 +2,34 @@
|
||||
|
||||
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. On iOS, the flow also surfaces as a Live Activity: the Lock Screen and the Dynamic Island show the recording's
|
||||
state and timer, with buttons that pause, resume, send, or discard it without opening the app.
|
||||
sheet. On iOS, the flow also surfaces as a Live Activity, so the recording can be followed and controlled from the
|
||||
Lock Screen and the Dynamic Island without opening the app.
|
||||
|
||||
A screen recording of the full flow is included in the repo: [`Attendi_Sample_App.MP4`](Attendi_Sample_App.MP4).
|
||||
A screen recording of the end-to-end flow on iOS ([direct link](.README/Attendi_Demo.MP4)):
|
||||
|
||||
| Full recording flow on iOS |
|
||||
| --- |
|
||||
| <video src=".README/Attendi_Demo.MP4" controls width="320"></video> |
|
||||
|
||||
Live Activity interactions can't be captured in a screen recording, so these screenshots stand in — the permission prompt on first record, the active and paused Lock Screen banners, and the collapsed Dynamic Island:
|
||||
|
||||
| User permission | Microphone recording | Microphone paused | Dynamic Island |
|
||||
| --- | --- | --- | --- |
|
||||
|  |  |  |  |
|
||||
|
||||
On iPhone, starting a recording also starts a Live Activity, so the flow can be followed and controlled without the app
|
||||
in the foreground. On the Lock Screen it appears as a banner, and while the app is open it collapses into the Dynamic
|
||||
Island. Both surfaces show the recording's current state and a timer that the system counts up on its own — no app
|
||||
updates needed to keep it ticking — alongside buttons that pause, resume, send, or discard the recording. Tapping pause
|
||||
or resume acts silently in place; send and discard bring the app forward to finish the flow. Each button routes back into
|
||||
the same recording state machine as the in-app controls, so the Live Activity and the app never disagree, and the
|
||||
activity ends the moment the recording does.
|
||||
|
||||
## 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).
|
||||
healthcare professionals, based on [a given set of requirements](.README/Assignment.pdf).
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -24,8 +42,8 @@ healthcare professionals, based on [a given set of requirements](README.pdf).
|
||||
## Running the app
|
||||
|
||||
Open `Attendi.xcodeproj` in Xcode, pick the `Attendi` scheme, and run it on an iOS device or simulator, a Mac, or an
|
||||
Apple Vision Pro — the same code base covers all three. On an iPhone, starting a recording also starts a Live
|
||||
Activity: lock the device or leave the app to follow and control the flow from the Lock Screen or the Dynamic Island.
|
||||
Apple Vision Pro — the same code base covers all three. On an iPhone, lock the device or leave the app to follow and
|
||||
control the recording from its Live Activity, as described above.
|
||||
|
||||
Two things to expect on a first run: the app asks for the microphone permission when the first recording starts, and
|
||||
the first transcription in a language needs a network connection — picking a locale from the toolbar menu downloads
|
||||
@@ -140,8 +158,8 @@ no-op on the other platforms. Everything else, including the Liquid Glass stylin
|
||||
|
||||
- **Model-owned side effects** — a button press and a unit test drive the exact same transition code, keeping the
|
||||
views pure renderers and the state machine fully testable.
|
||||
- **Serialized service access** — a task chain guarantees the capture calls reach the service in transition order, no
|
||||
matter how quickly the user taps.
|
||||
- **Serialized service access** — separate task chains guarantee the capture and the reporting calls each reach their
|
||||
service in transition order, no matter how quickly the user taps.
|
||||
- **Drift-free timing** — the recording time derives from a monotonic clock anchor instead of counted ticks, so
|
||||
scheduling latency never accumulates.
|
||||
- **File-based audio hand-off** — the capture and the transcription exchange a file URL, never an in-memory copy of
|
||||
@@ -169,8 +187,9 @@ The flow is modeled as a state machine in `RecordingView.Model`, an `@Observable
|
||||
|
||||
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 discard button stays available while processing, where it cancels the in-flight transcription
|
||||
and throws the recording away without publishing anything. The model owns every side effect of a transition (timer,
|
||||
returning to idle. While processing, the send button shows a progress indicator and is disabled, and the discard
|
||||
button stays available — there it cancels the in-flight transcription and throws the recording away without
|
||||
publishing anything. 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.
|
||||
|
||||
@@ -207,8 +226,9 @@ distinguished from an ordinary transcription failure. The underlying errors are
|
||||
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
|
||||
Liquid Glass background — tinted by the color given at initialization, red by default — that reacts fluidly to
|
||||
presses, and dims while disabled. The label and its padding scale with Dynamic Type via `@ScaledMetric`.
|
||||
Liquid Glass background, tinted by the color given at initialization — the feature colors the main button yellow,
|
||||
discard red, and send green — that reacts fluidly to presses, and dims while disabled. The label and its padding
|
||||
scale with Dynamic Type via `@ScaledMetric`.
|
||||
|
||||
### Services
|
||||
|
||||
@@ -224,7 +244,8 @@ attached to its view model:
|
||||
locale ahead of its first transcription, emitting the started, cancelled, and failed downloads through an
|
||||
`events` stream instead of throwing.
|
||||
- **`Reporting`** — reports the lifecycle of the recording flow — started, paused, resumed, processing, and ended —
|
||||
outside the feature's UI; the app implements it over `ActivityKit` to drive the Live Activity.
|
||||
outside the feature's UI, plus a `reset` that clears any surface a previous run left behind; the app implements it
|
||||
over `ActivityKit` to drive the Live Activity.
|
||||
|
||||
The real backends used by the app are:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user