Dual-Source Step Tracking
Why Step FWD combines HealthKit and CMPedometer for step counting, and how the reconciliation logic keeps your numbers accurate.
Step counting should be simple. Your phone has a pedometer. It counts steps. Display the number. Done.
Except it’s not that simple. iOS gives you two ways to read steps, and they behave differently, update at different frequencies, and occasionally disagree with each other. Using just one leaves gaps. Using both requires a reconciliation system that most walking apps skip.
Two sources, different strengths
HealthKit is Apple’s health data store. It aggregates steps from all sources — your iPhone, Apple Watch, any third-party devices — and gives you a single authoritative total. It’s comprehensive and reliable, but it updates in batches. When you’re actively walking, HealthKit might be 30 seconds to several minutes behind your actual step count.
CMPedometer is the Core Motion pedometer. It reads directly from the iPhone’s motion coprocessor and updates in near-real-time, roughly once per second. It’s fast and responsive, but it only counts steps from the iPhone itself. If you’re wearing an Apple Watch and carrying your phone in a bag, CMPedometer might undercount because the phone isn’t moving with your stride.
Neither source is complete on its own. HealthKit is accurate but slow. CMPedometer is fast but narrow.
The hybrid model
Step FWD uses both, simultaneously. The approach is straightforward:
Displayed Steps = Baseline (HealthKit) + Delta (CMPedometer)
When the app initializes, it fetches the current day’s total from HealthKit. This becomes the baseline — the authoritative count that includes all sources up to that moment. Then CMPedometer starts streaming live updates. The delta from CMPedometer gets added to the baseline, giving the user a responsive count that updates in real time.
Periodically, HealthKit syncs and delivers an updated total. When the HealthKit total exceeds our current displayed count, we adopt it as the new baseline and reset the CMPedometer delta. This means the hybrid value never falls behind HealthKit’s authoritative count.
When the HealthKit total is lower than our hybrid value — which happens frequently, since CMPedometer runs ahead — we keep our value. The user’s displayed count is always the maximum of the two systems.
Never going backward
This is the critical invariant: the step count never decreases. Not when HealthKit syncs, not when the app returns to the foreground, not on a pedometer restart. Steps only go up.
We enforce this at every reconciliation point:
new_baseline = max(healthkit_total, current_baseline + current_delta, persisted_value)
If HealthKit delivers a number lower than what we’re showing, we ignore it. If the app restarts and loads a persisted value from disk, we take the maximum of that and whatever HealthKit reports. This prevents the disorienting experience of watching your step count jump backward after a sync.
Reconciliation triggers
The hybrid value reconciles at four points:
- HealthKit observer callback — HealthKit fires an observer query whenever new step data is written. This is immediate for iPhone steps and slightly delayed for Watch steps.
- App foreground — When the user returns to the app, we re-fetch from HealthKit in case updates arrived while backgrounded.
- Day rollover — At midnight, everything resets. Baseline goes to zero, delta goes to zero, CMPedometer restarts with a fresh start time.
- Manual refresh — An explicit reconciliation call available for pull-to-refresh gestures.
During a walking session
Active sessions add another layer. When you start a walk, the session captures a snapshot of your current step count as its baseline. CMPedometer then streams session-specific steps.
Pausing a session stops the pedometer and snapshots the current state. Resuming starts a fresh pedometer stream, and the new delta adds to the paused snapshot. This means pause/resume cycles don’t lose steps or double-count.
Session Steps = session_baseline + pedometer_snapshot_steps
The session baseline is captured once at start. Each pause captures cumulative pedometer steps into the snapshot. Each resume starts a new stream that adds to the snapshot.
Distance and calories
Distance comes from CMPedometer when available — it reports distance based on the accelerometer’s stride estimation. As a fallback, we calculate distance from steps using a height-based stride length formula: stride length equals height multiplied by 0.415 (a well-established biomechanical ratio), then distance is steps multiplied by stride length.
Calories are calculated at 0.04 calories per step, scaled by the user’s weight relative to a 70kg baseline. It’s an approximation, but it’s transparent — we don’t pretend to know your exact metabolic rate. The formula is simple enough that users can reason about it.
What we considered and rejected
Using only CMPedometer would be simpler — no reconciliation needed. But you’d miss Apple Watch steps entirely, and background step counting would depend on the iPhone being in motion. Too many gaps.
Using only HealthKit would be authoritative but sluggish. During an active walk, seeing your step count update every 30–60 seconds feels broken. Users expect responsive feedback.
Server-side reconciliation would let us merge data more intelligently. But it would require an account, a cloud service, and sending health data off-device. That’s a non-starter for our privacy model.
The hybrid approach is more complex to implement, but it gives users the best of both worlds: real-time responsiveness with authoritative accuracy.
Persistence
The current hybrid value is persisted to disk on every update. If the app is killed and relaunched, it loads the persisted value and takes the maximum of that and a fresh HealthKit fetch. The persisted value includes a date stamp — on a new calendar day, it’s discarded and the system starts fresh.
This prevents the edge case where a user force-quits the app, takes a walk tracked only by HealthKit, and relaunches to find their count has jumped up instead of starting from a stale persisted zero.
Every step counts, and every step should be counted. The reconciliation system exists to make sure of that, without asking you to think about where your steps came from.