Drift is a native iOS audio app for focus and relaxation. It generates binaural tones and ambient noise in real time instead of looping pre-recorded audio files, then pairs that sound with a calm SwiftUI interface and a lightweight mixer.
The project is intentionally small, but it touches a few areas that matter in production iOS work: real-time audio, Swift 6 actor isolation, observable UI state, custom drawing, and testable state transitions.
- Real-time binaural tone synthesis using
AVAudioEngineandAVAudioSourceNode. - Procedural rain and white-noise layers generated without bundled audio files.
- SwiftUI interface with frequency presets, animated background visuals, waveform drawing, and a mixer sheet.
- Swift 6-ready audio architecture that separates main-actor UI state from Core Audio render callbacks.
- Unit tests for the audio controller's default state, frequency changes, mixer values, and start/stop behavior.
- No accounts, network requests, analytics SDKs, or external services.
| Area | Implementation |
|---|---|
| Language | Swift 6 |
| UI | SwiftUI |
| State | Swift Observation with @Observable, @Bindable, and @State |
| Concurrency | @MainActor, nonisolated render factories, and Task loops for fades/LFO updates |
| Audio | AVAudioEngine, AVAudioSourceNode, AVAudioMixerNode, AVAudioSession |
| Drawing | Canvas, TimelineView, and Animatable for the waveform |
| Tests | XCTest unit tests for AudioController |
| Tooling | Xcode 17 |
Not currently used: SwiftData, remote APIs, dependency injection containers, or persistent storage. Drift does not need those pieces for its current scope, so the code keeps the architecture direct.
Drift is organized around a small SwiftUI app surface and one central audio controller.
MainViewowns the primary session UI: frequency tabs, play/pause, the animated wave, and the mixer entry point.MixerViewedits the public mixer values exposed by the audio controller.AudioControllerowns audio engine setup, synthesis nodes, playback state, volume fades, and the slow panning LFO.BrainwaveStatedefines the preset frequencies shown in the UI.WaveView,BackgroundView, andSpaceViewkeep visual rendering separate from audio logic.
This is not a heavy MVVM implementation with many view models. For this app, the useful separation is simpler: SwiftUI views handle presentation, while AudioController acts as the observable model/controller for audio state and engine behavior.
The binaural tone is generated sample-by-sample in an AVAudioSourceNode render callback. Rain and white noise are procedural too, using small render-thread state objects instead of audio assets. That keeps the app lightweight and avoids loop points.
AudioController is @MainActor because it drives UI-observed state. The Core Audio render callbacks are created through nonisolated factory methods so playback is not accidentally tied to the main actor or UI queue.
Render-thread values are kept outside observation with @ObservationIgnored. The current implementation uses small unchecked-sendable render state objects; this keeps Swift 6 strict concurrency happy while preserving the separation between UI state and the audio render path.
The public controller state is intentionally small:
controller.setFrequency(BrainwaveState.theta.centerFrequency)
controller.rainVolume = 0.25
controller.start()That makes the UI straightforward and gives the unit tests clear behavior to verify.
The waveform uses Canvas and TimelineView so the visual can animate independently from the audio engine. WaveView conforms to Animatable, which lets SwiftUI interpolate frequency and amplitude changes cleanly.
Drift is local-only in its current form.
- No sign-in.
- No network calls.
- No remote API handling.
- No analytics or tracking SDKs.
- No user-generated data storage.
- Audio is generated on device.
The app does configure an iOS playback audio session so sound can continue appropriately for a relaxation/focus experience.
The project includes lightweight XCTest coverage for the audio controller:
- default state
- frequency updates
- mixer volume updates
- idempotent stop behavior
- start/stop behavior when
AVAudioEnginestarts successfully
The current UI test target is intentionally left without app-launching tests because the simulator UI runner is expensive on the development machine used for this project. The unit tests are the practical default for local verification right now.
Latest verification run:
xcodebuild test \
-project Drift.xcodeproj \
-scheme Drift \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5' \
-derivedDataPath /tmp/DriftUnitTestsDerivedData \
CODE_SIGNING_ALLOWED=NO \
-only-testing:DriftTestsResult: TEST SUCCEEDED with all 5 DriftTests passing.
- Open
Drift.xcodeprojin Xcode 17. - Select the
Driftscheme. - Choose an iOS simulator or connected iPhone.
- Build and run with
Cmd+R. - Run unit tests with
Cmd+U, or run onlyDriftTestsfrom Xcode's test navigator.
Command-line build:
xcodebuild build \
-project Drift.xcodeproj \
-scheme Drift \
-configuration Debug \
-destination generic/platform=iOS \
CODE_SIGNING_ALLOWED=NODrift shows that I can take a focused product idea and build it with care across UI, audio, and Swift 6 readiness. The main technical work is not in adding lots of screens; it is in keeping real-time audio separate from UI isolation, using SwiftUI where it fits well, and adding enough tests to protect the controller behavior without overcomplicating a small app.
It is a compact project, but it reflects the kind of engineering judgment I value: understand the platform constraints, keep the architecture honest, and verify the parts most likely to break.
