Guitar Metronome - Case Study
A focused practice metronome for guitarists, built in SwiftUI with an AVAudioEngine audio core
Guitar Metronome
Oct 19, 2026 · @Gary
Overview
A focused practice metronome for guitarists, built in SwiftUI with an AVAudioEngine audio core. It does two things well: keeps rock-steady time, and walks you from a comfortable tempo to a target tempo over a practice session.
| Platform | iOS (iPhone) |
| Stack | SwiftUI, AVAudioEngine |
| Design | Figma, design-first, dark theme |
| Modes | Steady, Speed Trainer |
| Role | Solo: product, design, engineering |
The problem
I came back to guitar after a ten-year break, working on Gilmour solos where feel and timing matter more than speed. The App Store is full of metronomes, but most bury the two things I actually use under subscriptions, sound packs and setlists.
What practice needs is narrower:
- See the tempo at a glance, change it with one thumb, tap it in from a recording.
- Set a practice block (1 to 30 minutes) and stop thinking about the clock.
- Push a passage up to tempo gradually, without stopping to nudge the BPM every few minutes.
The brief became: do those three jobs with no friction, and never drift.
Design process
I designed every screen in Figma before writing any SwiftUI, then built against the frames. The app has three screen states across two modes: Steady (idle, and running with a timer) and Speed Trainer.
The decisions that shaped it:
| Decision | Why |
|---|---|
A Steady / Speed Trainer segmented control on every screen |
One app, two jobs; switching modes should never be more than one tap away |
| Timer preset chips reused as ramp-duration chips in trainer mode | Same muscle memory, same grid, a different meaning per mode |
| Tap-for-tempo removed in trainer mode | With a start and a target tempo on screen, it was ambiguous which one a tap would set |
Single tempo slider replaced by paired FROM / TO stepper cards |
Two values need two clear controls, not one overloaded slider |
| Tempo card shows live mid-ramp BPM with a progress track | You can see where you are in the ramp without reading a number |
One layout bug worth noting: on the trainer screen a flexible spacer collapsed to about 3px, crowding the controls. The fix was tightening the content column gap to 14pt and trimming the tempo card's padding, rather than fighting the spacer.
Engineering: drift-free timing
The one thing a metronome cannot get wrong is time, and the obvious SwiftUI approach gets it wrong. A Timer fires on the main run loop, so every click lands a few milliseconds late whenever the UI is busy, and the error accumulates.
Instead, clicks are scheduled against the audio hardware's own clock. Each beat is placed at an exact sample position on an AVAudioPlayerNode, computed from the tempo, not from when the previous callback happened to run:
let samplesPerBeat = AVAudioFramePosition(sampleRate * 60.0 / beatsPerMinute)
let beatSampleTime = startSampleTime + beatIndex * samplesPerBeat
let beatTime = AVAudioTime(sampleTime: beatSampleTime, atRate: sampleRate)
clickPlayerNode.scheduleBuffer(clickBuffer, at: beatTime)
The UI never drives the audio. It only reads the audio clock to animate the beat indicator, so a dropped frame on screen cannot become a late click in your ears.
Speed Trainer: stepped beats glide
Speed Trainer takes a start tempo, a target tempo and a duration, and moves you from one to the other while you play. My first design glided continuously, a fraction of a BPM every beat.
In practice that is the wrong default. A tempo that never settles gives your hands nothing to lock into; you are always slightly behind a moving target. The way teachers work is stepped: hold a tempo long enough to play the passage cleanly, then bump it.
So the ramp became a step-size control. A step of +5 holds each tempo for an even share of the duration before jumping; a step of +1 is effectively the old glide, kept for anyone who wants it. One parameter covers both behaviours with no extra mode to explain.
Because beats are scheduled on the audio clock, a tempo change takes effect on an exact beat boundary, never mid-beat.
Details and craft
- Icon. An amber pendulum on dark charcoal, with a reversed variant. The metronome body is deliberately left out so the mark stays legible at 60px; I checked it at 180, 120, 87 and 60px before committing. Masters are exported square, since iOS applies its own corner mask.
- BPM display.
.contentTransition(.numericText())rolls the digits as tempo changes, so the number reads as moving rather than flickering. - Hold to scrub.
.buttonRepeatBehavior(.enabled)on the steppers lets you hold to sweep tempo quickly, then tap for single steps. - Chip grid. Practice presets (1, 2, 3, 5, 10, 15, 20, 30 min) sit in a
LazyVGrid, sized for a thumb while holding a guitar.
Outcome and next
The result is a metronome I actually practise with: two modes, no settings screen to get lost in, and timing I trust. The main lesson carries over to any real-time iOS work: let the hardware clock own time, and keep the UI as an observer. This learning has already been applied on the TopThis project where it drives a countdown animation.
Gary McKiernan