# Attendi Sample App

A sample app that implements an audio recording flow in SwiftUI: start, pause, and resume a recording while a timer 
tracks the elapsed time, then send the recording for (simulated) processing.

## 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).

## Demo

[▶️ Watch the sample (iOS) app in action!](Attendi_Sample_App.MP4)

## 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)
│       ├── AttendiApp.swift         # The @main entry point
│       ├── ContentView.swift        # The root view, hosting the Recording feature
│       └── Assets.xcassets
└── Packages/
    └── Features/                    # Local Swift package with the feature code
        ├── Package.swift
        ├── Sources/
        │   └── Recording/           # The Recording feature target
        │       ├── RecordingView.swift
        │       ├── RecordingViewModel.swift
        │       ├── RecordingButtonStyle.swift
        │       ├── RecordingService.swift
        │       ├── TranscribingService.swift
        │       └── Images.xcassets  # Record, pause, and send icons
        └── Tests/
            ├── Recording/
            │   └── RecordingViewModelTests.swift
            └── 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 or sent. |
| `processing` | The sent recording is being processed. |

The main button starts, pauses, and resumes a recording. While paused, a send button appears; pressing it moves the 
flow into processing (currently simulated with a delay) before returning to idle.

### Timer

While recording, an async task increments the elapsed seconds once per second — pausing stops it, resuming continues 
it, and a new recording resets it. The view formats the count as `mm:ss` and animates digit changes with a numeric 
text content transition.

### Views

- **`RecordingView`** — the public entry point of the feature. It renders the timer label and the control buttons for 
     the model's current state, and forwards state changes back to the model.
- **`RecordingButtonStyle`** — a custom `ButtonStyle` used by the controls: the label sits on a padded, circular red 
     background, shrinks while pressed, 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:

- **`RecordingService`** — captures the audio from a microphone, with throwing async `start`, `pause`, `resume`, and 
     `stop` methods; `stop` returns the captured audio.
- **`TranscribingService`** — transcribes the captured audio into text through a throwing async `transcribe` method; 
     the view model stores the result once processing finishes.

The default `SimulatedRecordingService` and `SimulatedTranscribingService` fake the work (the latter with a two-second 
delay and a dummy transcription); the unit tests inject fast mocks, and real backends can be plugged in the same way 
without touching the feature's state machine. When either service fails, the view model falls back to the 
not-recording state.

## Testing

The view model is covered by Swift Testing suites in `RecordingViewModelTests.swift`, grouped with nested `@Suite` 
types (initial state, button presses, computed properties, timer, and processing). The state-dependent behaviors are 
exercised with parameterized tests across all four states.

Run the tests with the `Features` scheme in Xcode (`⌘U`), or from the command line:

```sh
xcodebuild test -scheme Features -destination 'platform=iOS'
```

## Tooling

- **`.swift-format`** — configuration for Apple's `swift-format`, used by Xcode's built-in formatter (4-space 
     indentation, 200-column lines).
