iOS · Computer vision · Game
PushUp Rivals
PushUp Rivals turns the front camera into a rep counter. Apple's Vision framework reads the body, a state machine decides what counts as a push-up, and the reps drive a battle on screen. There is no server, no account and no network call.
- Role
- Sole developer — pose tracking, rep detection, game logic, interface and test suite
- Year
- 2026
- Live
- App Store
- Swift
- SwiftUI
- Vision
- AVFoundation
- Speech
- Core Haptics
- Combine
- XCTest

What it does
Counting push-ups is the kind of problem that looks solved until you try it. A phone on the floor sees a body at a steep angle, in a hoodie, in bad light, with the wrists sometimes behind the hands and the hips lost in a jacket. Most of the frames are imperfect, and the count still has to be right — one phantom rep and nobody trusts the number again.
PushUp Rivals is built around that problem. The camera watches, the app counts, and the reps drive a fight on screen: push to attack, stop and you stop hitting. The game is the reason to keep going; the counter is the engineering.
Built for people who want a reason to keep doing push-ups and will not strap on a sensor or stop to tap a screen between sets.
What I did
I wrote the app on my own — the tracking pipeline, the rep detection, the game modes, the interface and the tests.
The part I would defend in an interview is the counter. It is a pure, deterministic state machine: every call takes an explicit timestamp, so it holds no wall-clock state and the same sequence of frames always produces the same count. That is what makes a camera feature testable at all — the suite replays joint positions and asserts the number.
Around it sits a quality model that decides whether a frame is usable, a smoother that takes the jitter out of Vision's output, and a voice path for when the body simply cannot be read.
Parts were written with AI assistance. The detection model, the thresholds and the failure handling are mine.
- Swift files
- 91
- XCTest methods
- 130
- Network requests
- 0
- Game modes
- 3
How it is put together
Everything runs on the phone. There is no backend in this product at all — no account, no sync, no analytics endpoint — so the pipeline below is the whole system.
Camera to counted rep
Capture session
AVFoundation frames from the front camera. They are read, used and dropped; none is written to disk or sent anywhere.
Body pose request
Vision returns joint positions with a confidence per joint. Confidence, not just position, is what the rest of the pipeline reasons about.
Smoothing
An exponential smoother over the elbow angle takes out the frame-to-frame jitter that would otherwise trip the thresholds.
Quality gate
A frame is usable when one arm chain — shoulder, elbow, wrist — is reliable enough, judged on the chain's mean confidence so two solid joints carry a weak one.
Rep state machine
A rep is counted on a complete top to bottom to top cycle, and only when two independent signals agree.
When the body cannot be read
Joints drop out
Dark clothing, a hood, a hand across the lens. Held joints keep the machine moving through a short grace window.
A held frame can never finish a rep
The confirming top must contain at least one freshly detected frame, so the app cannot invent a rep out of stale data.
Voice fallback
Speech recognition over the microphone. Any spoken word on the descent counts, so the user picks their own rhythm instead of a magic phrase.
One entry point
Camera and voice both call the same register-a-rep function, so the game never has two ideas about the score.
Both the pose pipeline and the speech pipeline are on-device Apple frameworks. Nothing in this app talks to a network.
What is in it
Two signals have to agree
The elbow angle has to cross widely separated top and bottom thresholds, and the shoulders have to actually travel downwards, measured against the body's own scale. Requiring both is what separates a push-up from lying on the floor bending your arms.
top 150° · bottom 105° · travel ≥ 0.16 of body scale
The face is deliberately ignored
No face, head, hip, knee or ankle landmark is read anywhere in the detection path. A hood, a mask or a jacket that hides the hips must never stop a rep counting, so the model only ever asks about arms and shoulders.
A dropped joint does not cancel your rep
Joints vanish constantly at the bottom of a push-up, where the wrists are hidden by the hands. Held joints keep the state machine alive for a grace period, and the deepest travel seen during an attempt is peak-held, so a single bad frame cannot undo a real descent.
grace 0.30 s · 2 stable frames to confirm
Voice as a real fallback
When tracking is hopeless — lying too low, too close, too dark — the user can count out loud instead. It is the same game, scored the same way, rather than a degraded mode.
Three modes on one counter
A campaign, a football ladder and a ranked match all read from the same rep source. Game modes differ in presentation and pacing, not in what counts as a push-up.
Ranked does not pretend to be multiplayer
The ranked opponent is a local simulator that schedules reps on a human-feeling rhythm, and the rating lives for the session only. There is no server and no matchmaking, and the code says so in as many words.
What was hard
Telling a push-up from a person bending their arms
Elbow angle alone is easy to cheat and easy to trigger by accident. Adding vertical shoulder travel, normalised by the body's own scale so it works at any distance from the camera, is what made the count trustworthy — with an exception for a deeply bent elbow, which confirms the bottom on its own when the torso cannot be read at all.
Making a camera feature unit-testable
Anything that reads a clock or holds live state is untestable in practice. The counter takes a timestamp with every frame and owns no other time, so the suite can replay a sequence of joint positions and assert an exact number. That decision is why 130 test methods exist at all.
Speech partial results are cumulative, not incremental
Apple's recogniser emits growing transcripts — “go”, “go now”, “go now yes” — and the segment timestamps are typically zero until the final result, so timing cannot tell you which words are new. Counting consumed segments positionally is the only reliable way to avoid scoring the same shout three times.
A recogniser that ends itself
The speech task stops periodically whether you want it to or not. The listening pipeline restarts itself transparently for as long as it is meant to be listening, and because callbacks arrive on an arbitrary queue, every published change and every rep it reports is hopped back to the main thread.
Where it stands
- On the App Store since July 2026 as a free download, requiring iOS 17 or later.
- 130 XCTest methods across 11 test files, covering the rep machine, the pose quality model, the voice command parser and the opponent pacer.
- No network code anywhere in the product: the camera, the recogniser and the game all run on the device.
Next case study
ReplyCurrent
A support assistant that answers from a website's own pages and documents.