From 0aaa25753339be18dd060f787153f2aff4af5098 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 5 Jul 2026 17:03:24 +0200 Subject: [PATCH] Updated the documentation on the README file in the project. --- README.md | 176 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 145 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index be9b0e7..02e57aa 100644 --- a/README.md +++ b/README.md @@ -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