Building StepWise

Two Weeks From First Commit to a Walking Coach experience

A build diary for a watchOS-first app, what shipped, what broke, and what my git history taught me.


The idea

StepWise is a simple pitch: your Apple Watch nudges you to take a short 5‑minute walk (target pace: a gentle 2 mph) at a steady rhythm through your work day. It tracks each walk live with a real HKWorkoutSession, saves it to Apple Health, and shows you how the week is going, right on the watch face, through two complications. This was all driven by a Story I heard on NPR. I’ve been walking for years and have leveled out on weight loss, and I figured this could help me.

The whole thing initial design was watch-first. The iPhone app that ships alongside it started life as nothing more than a distribution shell, because watchOS has no standalone App Store path, and Apple requires an iOS container to get a watch app onto TestFlight.

That “simple” idea took two weeks and 39 commits to get into testers’ hands. Here’s how it went.


Week 1, The unglamorous part: getting anything to ship via TestFlight

The first commit landed on July 2. It was already a working watch app: a workout engine, a reminder scheduler, shared storage in an App Group, and two WidgetKit complications. Simple, right?

And then I spent the next two days doing… none of that. Almost every commit from July 2–4 is distribution plumbing, what a big pain:

  • Aligning bundle identifiers with what App Store Connect expected.
  • Adding an iOS host app purely so the watch app could ship (Apple TN3157, watchOS can’t
    submit on its own).
  • Setting SKIP_INSTALL=NO so the watch app archived as a distributable product.
  • Declaring the non-exempt-encryption setting.

Lesson one: the code that makes the app work is often the smaller half of the job. The code, and configuration, that makes the app ship is the other half, and it’s the half nobody blogs about. This is why people specialize on their CI/CD pipeline.

By July 6 I got to build an actual feature: a configurable “delay” action on walk
reminders, so a badly-timed nudge could be pushed back instead of ignored. It came in as a feature branch and a pull request, the first moment the project felt like a project and not a scaffold.

July 8: the first TestFlight build went out to testers.


Week 2, part one, Making the companion app real

Once real people were tapping around, the thin iOS shell stopped being enough. Between July 11 and 14 the companion app grew up:

  • A metrics / weekly summary tab so you could see progress on the phone, not just the wrist.
  • A proper “How to use” guide.
  • A Settings screen that mirrors the watch.
  • A real app icon (Thank you to the team at – Designed by Vecteezy for the base vector).
  • A notifications permission check, because a reminder app that can’t send reminders is just a stopwatch.

This stretch also had my favorite kind of bug fix: “fix to number of walks miscount bug.” A walking app whose entire job is counting walks… was counting them wrong. The rule is subtle, a walk only counts toward your daily target if it runs the full five minutes; end early and it’s saved to Health but marked “Ended Early.” Getting that boundary exactly right, and only right, took a dedicated test file.

Lesson two: the core metric of your app is the thing you most need a regression test around, because it’s the thing users will notice being wrong first.


Week 2, part two, The wall: watch-to-phone sync

Then came the hard part. Every ambitious Apple-ecosystem project eventually hits
WatchConnectivity, and StepWise Improvements hit it on July 15.

The goal sounded reasonable: settings changed on the phone should reach the watch, and walks recorded on the watch should show up in the phone’s “This Week” tab. In practice this is a multi-day saga in the commit log:

  • “Attempted Sync Fixes”
  • “Attempting improved complication refresh and updates from non-StepWise walks”
  • “fix for crash”
  • “Fixed issues with Watch crashes and counts”
  • “Attempting…”, “Adding Debug tests”, “added fixes”

You can feel the grind in those messages. Somewhere in here I even committed the classic sign of a rough day, Sync thing conflict files (...sync-conflict-...swift) accidentally checked in, evidence of editing the same code across two machines.

Alongside sync, I added a genuinely nice feature: StepWise can now count walks recorded in other apps (like Apple’s Workout app) toward your daily target. An outside walk of any length counts as exactly one, a small rule that needed a surprising amount of test coverage to get right (short walks shouldn’t count; one long walk shouldn’t count as several; a StepWise walk shouldn’t be double-counted).


The three crashes that shipped

The most instructive part of the whole two weeks: three separate crashes made it all the way to TestFlight before I understood them. They’re now documented, permanently, in a test file called RegressionGuardTests.swift. All three came from the same corner of the codebase:

  1. MainActor isolation on a background queue. WCSession completion handlers created in a @MainActor context are inferred to be MainActor-isolated. But WCSession invokes them on its own queue, so the moment a reply arrived, the runtime’s isolation check killed the app. (Crash-on-reply, iOS 27.)
  2. Activating WCSession too early. Calling WCSession.activate() during App.init, before the app finished launching, meant the system could silently drop the activation. After that, WCSession ignored every re-activation for the entire process, sync was simply dead for the whole session, with no error. This is the one that left the phone’s “This Week” tab frozen on stale data.
  3. A permission sheet mid-workout. Requesting HealthKit workout read authorization in the walk-start path popped a permission sheet during workout start, which dismissed the watch app out from under the user.

Here’s the part I’m proud of. None of these can be reproduced in a normal unit test, they need a live WCSession on real hardware. So instead of pretending, I wrote tests that read the production source files and fail if the dangerous pattern comes back. They’re guards, not proofs. But every one of these shipped once, and none of them will ship twice.

Lesson three: when a bug can’t be caught by testing behavior, test the shape of the code. A source-pattern guard is ugly, and it’s a thousand times better than a crash you’ve already paid for.

There’s still a temporary yellow diagnostic strip in the TestFlight build showing the raw connection state (activationCallback=2 / liveState=2 / paired=true / lastReceive=...). Not elegant, but when a tester’s sync misbehaves, a screenshot of that strip plus pulled logs tells me in seconds what would otherwise take a day.


Ending on something human

The latest build (July 18) closes the loop in a way I really like. Turn on “Reflect With Apple Journal,” and 15 minutes after your last scheduled walk of the day, StepWise prompts you to capture how you feel in Apple’s Journal app, where your StepWise walks already appear as suggestions.

After two weeks of bundle IDs, isolation crashes, and connectivity state machines, it felt right to end on the feature that has nothing to do with plumbing and everything to do with why you’d take the walk in the first place.


What this taught me

  • Shipping is a feature. Roughly the first quarter of this project was configuration, not code. Budget for it.
  • Guard your core metric. The count is the product. Test its boundaries first.
  • WatchConnectivity is a state machine with sharp edges. Timing (when you activate) and isolation (which queue runs your closures) will both bite you, silently.
  • If you can’t test the behavior, test the code’s shape. Every crash that reaches production is a chance to write the guard that stops its return.
  • Real devices tell the truth. Simulators won’t give you heart rate, haptics, or the connectivity race conditions that actually shipped.

Two weeks, 39 commits, three crashes, and one small app that just wants you to get up and walk for five minutes. Worth it.


StepWise is a watchOS 11+ app written in Swift 6 and SwiftUI, using HealthKit, WidgetKit, and WatchConnectivity, no Combine, structured concurrency throughout. It’s currently in TestFlight. And planned to launch in August

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.