diff --git a/Attendi.xcodeproj/project.pbxproj b/Attendi.xcodeproj/project.pbxproj index 7ed3eb2..4ffb34c 100644 --- a/Attendi.xcodeproj/project.pbxproj +++ b/Attendi.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ /* Begin PBXFileReference section */ 02870A0D2FF7EB610079EA3A /* Attendi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Attendi.app; sourceTree = BUILT_PRODUCTS_DIR; }; 02870A262FF7F2990079EA3A /* README.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = README.pdf; sourceTree = ""; }; + 02870A322FF831CD0079EA3A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -58,6 +59,7 @@ 028709F12FF7E8E40079EA3A = { isa = PBXGroup; children = ( + 02870A322FF831CD0079EA3A /* README.md */, 02870A202FF7ECD00079EA3A /* Packages */, 02870A082FF7EB360079EA3A /* Apps */, 02870A232FF7F07F0079EA3A /* Frameworks */, diff --git a/README.md b/README.md new file mode 100644 index 0000000..c63d521 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# 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. + +## 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 + │ └── 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. + +## 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). +- **`README.pdf`** — the original assignment requirements.