Updated the documentation on the README file in the project.
This commit is contained in:
@@ -4,6 +4,8 @@ A sample app that implements an audio recording flow in SwiftUI: start, pause, r
|
||||
timer tracks the elapsed time, then send the recording to be transcribed on device — and read the resulting text in a
|
||||
sheet.
|
||||
|
||||
A screen recording of the full flow is included in the repo: [`Attendi_Sample_App.MP4`](Attendi_Sample_App.MP4).
|
||||
|
||||
## Context
|
||||
|
||||
This sample app was created for a technical interview for a senior Mobile (iOS + Android) engineer role at
|
||||
@@ -13,10 +15,20 @@ 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 |
|
||||
| :--- | :--- |
|
||||
| Xcode | _26_ or later |
|
||||
| Swift | _6.3_ or later |
|
||||
| Platforms | _iOS_, _macOS_, and _visionOS 26_ or later |
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
its speech model in the background, reported through the in-app notifications, and a recording sent before the
|
||||
download finishes simply waits for it. Every transcription after that happens entirely on device.
|
||||
|
||||
## Project structure
|
||||
|
||||
@@ -27,31 +39,97 @@ 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
|
||||
│ ├── Sources/
|
||||
│ │ ├── App/ # The @main entry point
|
||||
│ │ ├── View Models/ # The attached services, the supported locales, and the download-to-notification mapping
|
||||
│ │ └── Views/ # The root view: hosts the feature, the locale picker, the notifications, and the transcription sheet
|
||||
│ ├── Previews/ # The PreviewPreinstalling service backing the previews
|
||||
│ ├── Tests/ # The AttendiTests bundle: the content view model suite, with its mock
|
||||
│ ├── 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/
|
||||
│ ├── Notifying/ # The Notifying feature target
|
||||
│ │ ├── Models/ # The AppNotification model
|
||||
│ │ ├── Styles/ # The NotificationLabelStyle label style
|
||||
│ │ └── View Models/ # The Notifier view model
|
||||
│ └── 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
|
||||
│ ├── Errors/ # The AudioCapturingError, AudioTranscribingError, RecordingError errors
|
||||
│ ├── Models/ # The Transcription model
|
||||
│ ├── Protocols/ # The Capturing, Preinstalling, Transcribing protocols
|
||||
│ ├── Services/ # The AudioCapturing, AudioTranscribing, and AssetPreinstalling services, with its respective development dummies
|
||||
│ ├── Styles/ # The RecordingButtonStyle button style
|
||||
│ ├── View Models/ # The RecordingViewModel view model
|
||||
│ └── Views/ # The RecordingView view
|
||||
└── Tests/
|
||||
├── Recording/ # The model and view model suites, with their mocks
|
||||
├── Notifying/ # The notifier suite
|
||||
├── Recording/ # The model, view model, and service 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.
|
||||
|
||||
## Architecture
|
||||
|
||||
The project follows an MVVM architecture on SwiftUI, split across a thin app target and a local Swift package:
|
||||
|
||||
- **Views** render state and forward user events; they own no logic beyond layout, styling, and presentation, and
|
||||
decompose into private subviews and reusable styles.
|
||||
- **View models** (`@Observable`, `@MainActor` classes) own the state and every side effect of its transitions — the
|
||||
recording state machine and the notification lifecycle in the package; the locale and the mapping of download
|
||||
events to notifications in the app.
|
||||
- **Services** sit behind protocols (`Capturing`, `Transcribing`, `Preinstalling`) injected at initialization, so the
|
||||
same logic runs against the real audio backends, the preview dummies, or the test mocks.
|
||||
- **Models** are plain value types (`Transcription`, `AppNotification`) with injectable identifiers, so tests can
|
||||
construct comparable values.
|
||||
|
||||
The package boundary enforces the separation: the app depends on the `Features` library and composes its public
|
||||
surface, while everything else in the `Recording` target stays internal.
|
||||
|
||||
### Platforms
|
||||
|
||||
A single multiplatform code base targets iOS, macOS, and visionOS 26. Platform differences are isolated behind
|
||||
conditional compilation: the shared `AVAudioSession` — its configuration, deactivation, and interruption observation —
|
||||
only exists on iOS and visionOS, and the navigation bar title display mode is skipped on macOS. Everything else,
|
||||
including the Liquid Glass styling, is shared.
|
||||
|
||||
### Techniques
|
||||
|
||||
- **Swift 6 strict concurrency** — async/await throughout, `@MainActor` isolation for the UI-facing types, `Sendable`
|
||||
protocols and models, `AsyncStream` for service events, and `withTaskCancellationHandler` for cancellable
|
||||
downloads. The Combine framework is not used.
|
||||
- **Observation** — view models are `@Observable`, so views react to exactly the properties they read.
|
||||
- **On-device speech** — the `SpeechAnalyzer`/`SpeechTranscriber` stack with `AssetInventory` asset management; no
|
||||
audio ever leaves the device.
|
||||
- **Liquid Glass design** — custom `ButtonStyle` and `LabelStyle` types over `glassEffect` backgrounds, scaling with
|
||||
Dynamic Type via `@ScaledMetric`.
|
||||
- **Localization and accessibility** — string catalogs with generated string symbols on both targets, localized
|
||||
accessibility labels on every icon-only control, timer values spelled out in full units, and the transient
|
||||
notifications announced to assistive technologies when posted.
|
||||
- **Logging** — `OSLog` subsystems for the app and the package, so failures that are not surfaced remain diagnosable.
|
||||
|
||||
### Strategies
|
||||
|
||||
- **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.
|
||||
- **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
|
||||
the audio.
|
||||
- **Preflighted downloads** — speech model assets install when a locale is picked, as a cancellable task reported
|
||||
through notifications, instead of mid-transcription; locale reservations are recycled to respect the system's
|
||||
per-app quota.
|
||||
- **Typed error surfacing** — service failures map to a small user-facing error enum with tailored recovery, such as
|
||||
opening the app's settings for a denied permission, while the underlying errors go to the log.
|
||||
- **Behavior-driven tests** — Swift Testing suites drive the model exclusively through its public API, with
|
||||
parameterized cases across every state and mocks that record their invocations in call order.
|
||||
|
||||
## The Recording feature
|
||||
|
||||
### State machine
|
||||
@@ -67,13 +145,16 @@ 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 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.
|
||||
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,
|
||||
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.
|
||||
press of the main button would; once the interruption ends, the recording resumes by itself — but only when the
|
||||
system hints the capture may continue, and never a recording the user paused themselves.
|
||||
|
||||
### Timer
|
||||
|
||||
@@ -110,6 +191,9 @@ attached to its view model:
|
||||
(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`.
|
||||
- **`Preinstalling`** — resolves the locales the transcriber supports and preinstalls the speech model assets of a
|
||||
locale ahead of its first transcription, emitting the started, cancelled, and failed downloads through an
|
||||
`events` stream instead of throwing.
|
||||
|
||||
The real backends used by the app are:
|
||||
|
||||
@@ -117,8 +201,11 @@ The real backends used by the app are:
|
||||
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.
|
||||
in the closest supported equivalent of the requested locale, installing any missing speech model assets through
|
||||
the preinstalling service before the analysis.
|
||||
- **`AssetPreinstalling`** — downloads speech model assets through the system's `AssetInventory`, recycling the
|
||||
locale reservations beforehand since the system only permits a limited number per app, and discriminating a
|
||||
cancelled download from a failed one.
|
||||
|
||||
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
|
||||
@@ -128,30 +215,57 @@ be plugged in the same way without touching the feature's state machine.
|
||||
|
||||
`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.
|
||||
the locale of the spoken language from the locales the `Preinstalling` service 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 through the service, so the first transcription does not have to download them mid-processing.
|
||||
|
||||
The preinstallation runs as a cancellable task owned by the model: picking another locale cancels a download still in
|
||||
flight before starting the new one. The model maps the service's started, cancelled, and failed download events to the
|
||||
`Notifying` feature's `Notifier` — supplying the localized message and the symbol for each — which presents them as
|
||||
transient in-app notifications: `Label`s wearing the `NotificationLabelStyle`, a capsule Liquid Glass banner whose icon
|
||||
is tinted by the notification's kind (green for info, yellow for a warning, red for an error). The banners overlay the
|
||||
feature just below the navigation bar, sliding in from the leading edge and dismissing themselves after a few seconds,
|
||||
and every posted notification is announced to assistive technologies. A download that completes posts none.
|
||||
|
||||
The app itself no longer touches the `Speech` or `Accessibility` frameworks: all speech model management lives behind
|
||||
the package's `Preinstalling` service, and the notification lifecycle — posting, announcing, and auto-dismissal —
|
||||
behind the `Notifier`.
|
||||
|
||||
## 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
|
||||
The package is covered by Swift Testing suites. In the `Recording` target, `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. In the
|
||||
`Notifying` target, `NotifierTests.swift` covers the posting and auto-dismissal lifecycle with an injected, shortened
|
||||
dismissal delay, and `AppNotificationTests.swift` covers the `AppNotification` 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.
|
||||
service mocks record their invocations in call order. `AudioCapturingTests.swift` covers the one piece of pure logic in
|
||||
the audio services — the parsing of audio session interruption notifications into capturing events, extracted so it
|
||||
compiles on every platform. The rest of the system-bound service code, `AssetPreinstalling` included, is deliberately
|
||||
untested: thin wrappers over device-global frameworks like `AssetInventory` and `SpeechAnalyzer`, whose behavior mocks
|
||||
cannot meaningfully verify.
|
||||
|
||||
Run the tests with the `Features` scheme in Xcode (`⌘U`), or from the command line:
|
||||
Run the package tests with the `Features` scheme in Xcode (`⌘U`), or from the command line. The suites run on any of the
|
||||
three platforms; macOS needs no simulator:
|
||||
|
||||
```sh
|
||||
xcodebuild test -scheme Features -destination 'platform=iOS'
|
||||
xcodebuild test -scheme Features -destination 'platform=macOS'
|
||||
```
|
||||
|
||||
For iOS, point the destination at a booted simulator, for example `-destination 'platform=iOS Simulator,name=iPhone 16'`.
|
||||
|
||||
The app has its own `AttendiTests` unit-test bundle, hosted by the app: `ContentViewModelTests.swift` drives
|
||||
`ContentView.Model` with a mocked `Preinstalling` service, covering the locale loading and alignment, the
|
||||
cancellable preinstallation guard, and the mapping of the download events to the notifier's notifications. Run it
|
||||
with the `Attendi` scheme in Xcode (`⌘U`).
|
||||
|
||||
## 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.
|
||||
currently English only), resolved through Xcode's generated string symbols (`LocalizedStringResource`): the app's
|
||||
catalog covers the navigation titles, the transcription sheet, the locale picker, and the notification messages — the
|
||||
latter taking the locale's name as their argument — and the package's catalog covers the error alert and the
|
||||
accessibility labels.
|
||||
|
||||
## Tooling
|
||||
|
||||
|
||||
Reference in New Issue
Block a user