Waavform

Open-source SwiftUI audio component I designed and built. One line to use, every pixel overridable.

Client
Open source
Role
Design and engineering
Year
2024
The Waavform player on iPhone in landscape: a full-width audio waveform with the played portion filled dark, a blue playhead, current time and duration labels, and play/stop controls

Context

Waavform is a waveform-based audio player component for SwiftUI, published as an open-source Swift package. Drop it into an iOS project, point it at an audio file, and you get a player that draws the waveform, tracks position, and handles transport.

It’s a small project, a component rather than a product. I’m including it because the design problem in a component is different from the one in an app, and it’s the clearest example of my work where the interface and the API are the same decision.

The problem

SwiftUI ships no waveform player. Anyone who needs one is choosing between AVPlayer with a plain progress bar, which tells you nothing about the audio, or building waveform rendering, hit-testing and playback sync themselves, which is days of work for what most apps treat as a minor UI element.

The component design problem underneath: a player like this has to be usable in one line by someone who wants a sensible default, and completely overridable by someone who has a design system to match. Those pull in opposite directions. Aim at the first and you build something nobody can fit into a real app; aim at the second and the simplest possible use requires twenty lines of configuration.

What I built

One line is the whole API.

Waavform(audio: "TheMoon", type: "mp3")

That renders a working player with waveform, transport, timecode and view switching. Nothing else is required. Every other parameter is optional and named, so the component gets more configurable the further in you go rather than demanding decisions up front. The default is the product, and customisation is opt-in.

The waveform is the scrubber. There’s no separate progress bar. Played audio is filled in the progress colour, unplayed audio sits in the background colour, and the boundary between them is the playhead. One element is the display, the position indicator and the control. It also means the thing you’re scrubbing through shows you where you’re going. You can see the quiet part coming.

Time labels ride the playhead. Current time sits on the playhead and moves with it; duration pins to the end of the waveform. Most players park a timecode row underneath, which makes you look in two places. Attaching the number to the thing it describes costs nothing and removes a lookup.

The Waavform player during playback: the filled portion of the waveform grows as the playhead advances and the current-time label travels with it

Playback in the linear view. The filled region and the time label are the same mechanism. There is no separate progress indicator to keep in sync.

Two views, because there are two questions. Linear draws the whole track at once: how far through am I, where are the loud parts, what’s the shape of this thing. Scroll fixes the playhead and moves the waveform past it at higher magnification: what’s happening right now. Neither is the better default, so it’s a toggle, and the toggle is a text link labelled with its destination (“Scroll” when you’re in linear), so you never have to work out which state you’re in.

Three Waavform players stacked vertically on a dark iPhone screen, each with its own track title, progress colour and playback state
Three instances, independent state and colour. One is playing, one paused, one in scroll view. The component composes rather than assuming it owns the screen.

It can also be a rendering primitive. showTransport and showScroll hide the built-in chrome, and play(), pause(), stop() and toggleView() are exposed, so a host app can draw its own controls and drive the waveform directly. The same component serves someone who wants a finished widget and someone who only wants the hard part.

Technology

  • Swift and SwiftUI, distributed as a Swift Package Manager package for iOS
  • The public surface is one view with defaulted parameters, so a single line gets a working player and every visual decision stays overridable

Where it landed

Published as a Swift package in January 2024. Small numbers, a couple of dozen stars and a handful of forks, which is what a focused component in a narrow niche looks like. I’d rather say that than dress it up.