Skip to content
Aiden Azraya, home

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
  • Swift
  • SwiftUI
  • Vision
  • AVFoundation
  • Speech
  • Core Haptics
  • Combine
  • XCTest
The app tracking a push-up: a skeleton overlay drawn over the arms and shoulders, a Tracking indicator, and a rep counter below.
A capture from the device during a set, cropped to the tracking overlay and the counter. The skeleton is drawn from the joints Vision returns each frame.

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

  1. Capture session

    AVFoundation frames from the front camera. They are read, used and dropped; none is written to disk or sent anywhere.

  2. 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.

  3. Smoothing

    An exponential smoother over the elbow angle takes out the frame-to-frame jitter that would otherwise trip the thresholds.

  4. 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.

  5. 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

  1. Joints drop out

    Dark clothing, a hood, a hand across the lens. Held joints keep the machine moving through a short grace window.

  2. 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.

  3. 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.

  4. 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.