diff --git a/README.pdf b/.README/Assignment.pdf similarity index 100% rename from README.pdf rename to .README/Assignment.pdf diff --git a/.README/Attendi_Demo.MP4 b/.README/Attendi_Demo.MP4 new file mode 100644 index 0000000..9bc2f78 Binary files /dev/null and b/.README/Attendi_Demo.MP4 differ diff --git a/.README/Attendi_Demo_Dynamic_Island.PNG b/.README/Attendi_Demo_Dynamic_Island.PNG new file mode 100644 index 0000000..a73ac8c Binary files /dev/null and b/.README/Attendi_Demo_Dynamic_Island.PNG differ diff --git a/.README/Attendi_Demo_Live_activity_Allow.PNG b/.README/Attendi_Demo_Live_activity_Allow.PNG new file mode 100644 index 0000000..6d33d4d Binary files /dev/null and b/.README/Attendi_Demo_Live_activity_Allow.PNG differ diff --git a/.README/Attendo_Demo_Live_Activity_Paused.PNG b/.README/Attendo_Demo_Live_Activity_Paused.PNG new file mode 100644 index 0000000..0c208ef Binary files /dev/null and b/.README/Attendo_Demo_Live_Activity_Paused.PNG differ diff --git a/.README/Attendo_Demo_Live_Activity_Recording.PNG b/.README/Attendo_Demo_Live_Activity_Recording.PNG new file mode 100644 index 0000000..ca72b60 Binary files /dev/null and b/.README/Attendo_Demo_Live_Activity_Recording.PNG differ diff --git a/Attendi_Sample_App.MP4 b/Attendi_Sample_App.MP4 deleted file mode 100644 index 2e9d02c..0000000 Binary files a/Attendi_Sample_App.MP4 and /dev/null differ diff --git a/README.md b/README.md index 6ebde31..e4636cd 100644 --- a/README.md +++ b/README.md @@ -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 | +| --- | +| | + +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 | +| --- | --- | --- | --- | +| ![User permission](.README/Attendi_Demo_Live_activity_Allow.PNG) | ![Microphone recording](.README/Attendo_Demo_Live_Activity_Recording.PNG) | ![Microphone paused](.README/Attendo_Demo_Live_Activity_Paused.PNG) | ![Dynamic Island](.README/Attendi_Demo_Dynamic_Island.PNG) | + +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: