Built in Swift · Shipped on the App Store

More Miles: What Happens When a Non-Technical Person Ships a Running App

More Miles mascot running along an elevated track
A running app about the life around the run, not just the recording.

TL;DR

I wanted one honest place to see whether my running was actually improving. So I built it.

More Miles is a native iPhone app that turns approved Apple Health workouts into clear progress, personal benchmarks and an optional on-device roast. It has no account, no manual logging and no health-data server. This article covers the health scare that got me running, the product decisions, Effort v4 and what it took to ship the whole thing myself.

In 2022, a lung infection put me in hospital for a week and left my lung capacity in a pretty bad state.

My hand with an IV line during a hospital stay in 2022
2022. Not exactly the beginning of a running era.

After that, hiking, running and swimming made me breathless far too quickly, so I avoided aerobic exercise for a long time. More recently, after leaving work and discovering a funny collection of small issues at a health check, I decided to take both mental and physical health more seriously.

I started running with friends who already had the habit. That made it less like a chore and more like a shared rhythm: comparing terrible splits, talking about how our legs felt and wondering why an easy run could suddenly feel harder than a race.

I also started caring about heart rate, cadence and progress over time. The data existed, but it was scattered across Strava, Apple Fitness and whatever else someone used to record. We could see each run, but not the training story behind it.

That frustration became More Miles.

More Miles is now live on the App Store. It is a native iPhone app that turns approved running and walking workouts from Apple Health into one clear, private dashboard. No account. No manual logging. No health-data server. Everything is processed locally.

This is the story of how it got built — and what I learned shipping it as a non-technical builder.

The product idea

I did not want to build another recorder.

If you run with an Apple Watch or another app that writes to Apple Health, the workout already exists. The problem is what happens afterward. The run gets buried in a list. Metrics depend on which app recorded it. Social apps are great for visibility, but not always for personal context. Some dashboards show numbers without explaining whether a run was demanding for you, on that day.

So More Miles started with a narrower promise:

Your running life, in focus.

It would not try to replace Strava, Garmin, Nike Run Club or Apple Watch. It would sit after the recording and organize what Apple Health already knows: distance, time, pace, heart-rate samples, cadence, running dynamics, splits, and workout metadata.

That decision shaped everything. More Miles could be automatic instead of manual. It could avoid inventing metrics when data was missing. And it could be private by default because it never needed a cloud profile.

More Miles total distance and distance trend screen
Thirty days, one readable story.
More Miles heart rate trend and personal records screen
Progress beyond a single run.

Design direction

The design brief was simple to say and difficult to execute: native, calm, expressive, and honest.

I wanted More Miles to feel like an iPhone app, not a web view pretending to be one. The app is built with SwiftUI, SwiftData, HealthKit and Swift Charts. It uses native navigation, system behaviours, haptics, light/dark appearance and typography as part of the product identity.

The mascot gives the app personality, but it does not take over. The data remains the protagonist. The mascot appears in records, roast moments and shareable cards, adding warmth without turning the dashboard into a game.

The hardest design problem was restraint. There were many tempting directions: more charts, more badges, more gamification, more social mechanics. But the product works best when it explains what is verifiably present and stays quiet when something is not. If a third-party app did not store a metric, More Miles does not pretend it exists.

More Miles mascot running through the app logo
Automatic by design. Private by default.

The build process

I used Codex as the primary development partner, with some Kimi Code for parts of the workflow. I also wrote a PRD before going deep into implementation.

That last part matters.

As a non-technical person, the PRD became my steering wheel. It forced me to answer questions before they became expensive: What is version 1.0? What is explicitly not in version 1.0? What does “private by design” mean in practice? How should missing HealthKit data be handled? What should the app never do?

The first version had to support a lot of unglamorous infrastructure:

  • one-time Apple Health connection;
  • historical run/walk import;
  • duplicate reconciliation when multiple apps record the same workout;
  • anchored incremental sync;
  • background observation for new workouts;
  • safe handling of deleted Health workouts;
  • versioned local caches for detailed heart-rate and pace samples;
  • empty states when permissions or metrics are unavailable.

None of that looks exciting in a screenshot, but it is the difference between a demo and a real product.

The main stack ended up as:

UI
SwiftUI and Swift Charts
Persistence
SwiftData, with sensitive imported health data excluded from backups
Health integration
HealthKit read access and background delivery
Models
Run records, goals, gear, zone settings and sync state
Website
React and TypeScript, deployed through GitHub Pages

Designing Effort

The feature I thought about most deeply was Effort. Most running apps already show pace. Many show heart rate. Fewer answer the question I actually care about after a run:

How demanding was this run for this runner at that time?

That is not the same as “how fast were you?” A hot, humid easy run can be harder than a cool tempo run. A short run with poor heart-rate coverage should not receive the same confidence as a complete recording. And a new user should still get a useful score before the app has learned their baseline.

The current implementation is Effort v4. An older internal document still carries a V3 filename, but the source itself uses algorithmVersion = 4. Effort applies only to runs of at least ten minutes and produces a score from 1 to 100. It is not Fitness, Readiness, Recovery or a judgement of running form.

Included
Duration, physiological intensity, heart-rate quality and conservative pace context.
Excluded
Cadence, ground contact time, vertical oscillation and stride length.
Distance
Not a direct weight. It contributes only through running speed context.

01 · Personalising heart rate

The app creates a heart-rate bin every five seconds and converts it into Heart Rate Reserve:

HRRt = clamp((HRt − HRrest) / (HRmax − HRrest), 0, 1.05)

Resting heart rate comes first from the median of daily resting-heart-rate values over the previous 28 days. If fewer than seven days are available, the window expands to 90 days. If that still fails, the app temporarily uses 60 bpm.

Maximum heart rate follows a similar hierarchy: a user-defined value, a recent reliable sustained peak, the age formula 208 − 0.7 × age, and finally the workout maximum as a temporary fallback.

02 · Nonlinear intensity

One hard minute should cost more than one easy minute. Each five-second interval uses a nonlinear weight:

w(x) = 0.25x, when x < 0.50
w(x) = x2.5, when 0.50 ≤ x < 0.85
w(x) = 1.15x2.5, when x ≥ 0.85

High-intensity exposure adds another small bonus: zero below 0.80 HRR, 0.20 from 0.80 to 0.90, and 0.50 at or above 0.90. The heart-rate load becomes:

LHR = Σ Δtt [w(HRRt) + b(HRRt)]

Time is measured in minutes. The result deliberately makes sustained high physiological intensity disproportionately expensive.

03 · Knowing when heart-rate data is weak

More Miles does not treat sparse or jumpy heart-rate samples as complete truth. Let ρ be heart-rate coverage and j be the proportion of neighbouring records that jump by more than 30 bpm:

c = clamp(ρ [1 − min(3j, 0.35)], 0, 1)

If a workout contains only an aggregate average heart rate and no detailed series, confidence is capped around 0.30 to 0.40. To enter the personal baseline, a run must last at least ten minutes, provide at least 80% heart-rate coverage, reach confidence of 0.65, and support a valid HRR calculation.

04 · A conservative pace fallback

This is why a run can still receive an Effort score without heart-rate data. The app looks at the user’s median speed across previous valid runs:

r = clamp(0.62 × v / vmedian, 0.42, 0.92)Lpace = durationminutes × r2.2Lblend = cLHR + (1 − c)Lpace

With no speed history, r starts at a conservative 0.55. With no heart rate at all, Lblend = Lpace. The run still gets a useful global score, but it cannot train the personal heart-rate baseline or claim a high-confidence benchmark.

05 · Cardiac drift

The app compares the first and second halves of a run using Cost = HRR / Speed. If similar speed demands more heart rate later, cardiac drift has appeared:

d = Costsecond / Costfirst − 1Mdrift = clamp(1 + 0.5(d − 0.03), 1, 1.08)

Only drift beyond 3% changes the score, and the modifier is capped at 8% so it can add context without taking over the result.

06 · Personal heart rate at a similar speed

For runs within ±12% of the current speed, the app looks for at least three valid historical sessions. It compares the current HRR with the expected HRR and converts the difference back into beats per minute:

ΔHR = (HRRcurrent − HRRexpected)(HRmax − HRrest)Mcontext = clamp(1 + 0.01ΔHR, 0.90, 1.10)

At the same speed, five extra bpm adds roughly 5%; five fewer removes roughly 5%. The adjustment is capped at ±10%.

07 · Raw session load

Lraw = max(0, Lblend × Mdrift × Mcontext)

The app stores the intermediate values too: heart-rate load, high-intensity load, confidence, drift modifier, pace-to-heart-rate modifier, raw load, personal percentile and personalisation weight. The score can therefore be explained and debugged instead of living as a black box.

08 · Mapping load to 1–100

During cold start, raw load follows a global segmented curve with linear interpolation between points:

Raw load5101828405575100
Effort12254055708292100

After at least five valid records, More Miles also calculates where the current load sits in the runner’s own history:

p = [N(Li < L) + 0.5N(Li = L)] / N

The 25th, 50th, 75th, 90th and 99th percentiles map approximately to personal scores of 25, 45, 65, 80 and 98. Up to 60 prior valid sessions are considered. The final score blends global and personal interpretations:

Effort = clamp[(1 − λ)G(Lraw) + λP(p), 1, 100]
Valid records510152030
Personal weight20%50%75%90%100%

The principle was: be useful from the first run, but become more personal over time.

What makes it different

  1. It is not distance × pace. The core idea is time × personal physiological intensity.
  2. It understands data quality. A few scattered third-party samples are never treated as complete heart-rate truth.
  3. Cold start and personalisation coexist. The first run gets a score; later runs gradually teach the app what hard means for this runner.
  4. History does not move its own goalposts. An old Effort score remains a description of how hard that session was then. It changes only when the algorithm version or underlying HealthKit workout changes.
  5. Load and performance stay separate. Effort asks “how demanding was it?” Personal Benchmark compares pace and heart rate across similar distances over 180 days to ask “how well did I run?”
  6. Fallbacks are conservative. Runs without heart rate still receive a useful low-confidence score without polluting the personal HR baseline.

That combination is a product and engineering decision, not a clinically validated medical algorithm. More Miles is closer to a local, explainable training-load system that gradually learns its runner than a dashboard that simply puts prettier charts around distance.

Run Roasting

My favourite feature is also the one no running product really needed, which is precisely why it belongs here: Run Roasting.

It was not in the original product blueprint. My friends Bryan and Jiale kept insisting that the app needed something with the energy of Garmin relentlessly judging you, as if running faster or farther would ever be enough to earn its approval. They were right.

After a run, you can summon a small antagonist mascot. It reads verified context from your run and roasts your training choices entirely on-device. It does not invent metrics. It does not attack your body, identity or worth. It targets the session — the skipped warm-up, the overly ambitious pace, the “this was definitely an easy run” decision.

More Miles roast mascot wearing sunglasses and a navy tracksuit
Roast mode
More Miles mascot lowering its sunglasses with a judging expression
Judging mode

The idea also connected to something I had built while working at Moonshot AI: a Discord bot plugged into Kimi’s latest LLM, with memory, live web search and file handling. It understood Gen Z slang, internet humour and how to be funny without trying too hard. People loved Kimi Bot because it did not feel like a corporate assistant. It had a personality. If you are curious, the build notes are here ↗

That experience made me think a similar energy could work in a running app. Running data can feel clinical. A roast turns it into a story you actually want to share.

It also creates a natural form of UGC. The share card includes real run facts and the mascot’s verdict, so people can post something funny, personal and still grounded in their data. Because the generation runs locally, the feature fits the product’s privacy model instead of quietly breaking it.

Beta testing

The early beta testers were running friends, which was exactly what the product needed.

They did not just say “nice app.” They gave specific feedback: this chart is confusing, this empty state feels dead, this roast is funny but this one goes too far, this metric should be here, this interaction needs haptics, this share card should look better in dark mode.

That feedback shaped the first release. It also reminded me that beta testing is not validation theatre. The goal is not to hear that your idea is good. The goal is to find the places where the product pretends to be finished before it actually is.

Deployment and App Store

The final stretch was its own education.

I already had the app working, but shipping meant dealing with signing, build numbers, TestFlight, App Store screenshots, metadata, privacy answers, review notes and support pages. I also had to make the public story precise: More Miles reads Apple Health data, processes it locally, does not require an account, does not include analytics or ads, and never writes or deletes your workouts.

The App Store forces useful clarity. You cannot hide behind vibes. You have to explain what data you use, why you use it, and what happens when something is unavailable.

What I learned

01

“Non-technical” does not mean passive

I did not hand-write every line of Swift from scratch. But I still had to understand the architecture, HealthKit behaviour, data models, privacy constraints and edge cases. AI coding tools reward people who can specify, review, test and make decisions. The prompt is not the job. Product judgement is.

02

The PRD is a debugging tool for thinking

Writing the PRD exposed contradictions before they reached the code. It made scope visible. It also gave Codex a much stronger context to work against.

03

Privacy is a product feature

No accounts and no health-data server were not just ethical choices. They made the product simpler and more trustworthy. Local-first design reduced infrastructure, removed a whole category of risk, and made the promise easier to explain.

04

Missing data is a design problem

Health data comes from many sources. Not every workout has complete heart-rate coverage. Not every app stores the same running dynamics. A good running dashboard has to be honest about gaps instead of fabricating a clean-looking story.

05

Personality needs boundaries

Run Roasting works because it is playful, but it also has rules. It uses verified context, avoids unavailable metrics, and does not make the run about shame or appearance. Humour is more durable when it punches at the training decision, not the person.

06

Shipping is the real feature gate

Every product feels close when the demo works. The last 10% — sync edge cases, duplicate workouts, permissions, empty states, accessibility, dark mode, App Store review — is where it becomes real.

What is next

Version 1.0 is only the starting line.

The current app focuses on the essentials: automatic Apple Health import, clear activity detail, personal context, goals, trends, gear mileage, shareable progress and Run Roasting. Future versions can go further, but only if they preserve the core promise: private, native, calm and useful.

For now, I am just happy the loop is complete.

I wanted a better way to understand running. I researched it, designed it, built it, broke it, tested it, refined it and shipped it.

Then, naturally, I let a mascot roast me about it.

Written, designed and built by Aspen Choong.