I’ve been a bit slow to update my blog series on trying to make a clone of Zwift, but not because I’ve stopped working on it. Rather, I’ve been able to use the MVP of what I’ve built so far in parts 1 and 2 and found that the time I spent working on my app could be used actually working out. I would write an implementation of something, but it would take so much of my time that I couldn’t test it out and I’d have to go to bed… Still, I was missing a few important features in my app, so I’ve been slowly working on them in between working on my fitness.
Hooking up the Apple Watch
One of the great things about Zwift is how much support they provide for different fitness accessories, including the Apple Watch. Unfortunately, the Apple Watch hardware is not set up to allow arbitrary Bluetooth connections like my exercise bike was in part 1. Instead, to access the user’s health information like heart rate, you’d need to write a full-blown WatchOS app!
Luckily, this wasn’t my first rodeo as I had worked on the Apple Watch app for Starbucks, so I was able to add a Watch app extension target in my project pretty quickly.
I Googled for how to get a user’s heart rate programmatically, came across a promising StackOverflow post with a link to a Github project and was able to get it implemented myself. However, as I looked at the copy-pasta code, it seemed sort of wrong to me. The code was starting a workout session, but then created an object query that would run a closure whenever a new heart rate sample (older than a certain date) was added by the workout. This seems like a roundabout way to get heart rate samples, and I wondered if Apple had a better API to accomplish this.
I ended up finding some Apple sample code that showed a better way to fetch heart rate data. The solution is to use some new features introduced in WatchOS 5 that allow for the creation of a workout directly on the Apple Watch. The Apple doc I linked explains it pretty well, but the steps are:
- Ask the user for permission to track their heart rate data
- Create a workout configuration (e.g. an indoor cycling workout) and a workout session, along with its associated workout builder
- Start the session and tell the workout builder to start collecting data samples
- Respond to the delegate method “workoutBuilder(_:didCollectDataOf:)” to collect a bunch of samples, including heart rate information
In code it looks something like this:
Rather than add some UI to the watch app to start a workout session, the iPhone version of HKHealthStore has a function called startWatchApp(with:completion:) which will send a workout configuration to the watch to facilitate the creation of a workout. All I need to do is call that function when my workout on the iPhone app starts and my watch app will respond by starting a HealthKit workout session which starts measuring things like heart rate (and calculating its own estimated calories burned).
I was now able to get the heart rate as the watch was reading it and update whatever display on the watch I wanted to. That was only half the story though. In Zwift the heart rate shows up in the user interface, and I wanted to mimic that myself. Since I couldn’t access the workout session directly from the phone I’d have to send the heart rate info back to the main app from the watch.
Back to the App
This blog post isn’t about Watch apps, so I won’t go over that aspect of this feature too much. I basically used the WatchConnectivity session to send messages back to the app with a dictionary containing the new heart rate.
And after all of that programming, I’d like to present you with the most difficult video I’ve ever shot: on an iPad, while balancing on an exercise bike, recording both my wristwatch with my heart rate AND the app showing the exact same heart rate!
I also rigged up an initial interface that shows which workout segment I’m on, the next segment coming up, the progress through the segment and my progress for the entire workout, along with stats like calories burned (determined by the bike), cadence and distance traveled.
At this point, I had a pretty functional app! But seeing the extensive APIs of HealthKit made me want to add more and more to my app. This is scope creep in action. See the documentation of HKWorkoutBuilder to see all of the data and metadata you can store. I ended up sending a few more messages from the app back to the watch so I could store more data to the workout:
At the end of a workout, I send the start and end times, along with the total calories burned and distance traveled. This isn’t really necessary because the watch already makes a guess about the calories burned and the distance isn’t real because it’s on a stationary bike. But I thought it might be interesting to see how that data is represented.
I also toyed around with sending segment data but I haven’t seen it visually represented anywhere in the workout view. I wanted to see more detail about the workout in the Apple Activity app so I also sent the name of the workout as the HKMetadataKeyWorkoutBrandName value, though I’m not sure that’s what it’s intended for! Here’s what the workouts look like in the Activity app and the Health app’s workout data:
One more fun but optional thing I thought of and added was a wrist tap reminder when I got close to the end of a segment. Sometimes I’m just in the zone and not paying attention to the fact that I need to ramp up or ramp down for the next section, so when there are 5 seconds left in a segment I send a message to the watch from the phone to tap my wrist and send a reminder:
One of the nice things about writing your own workout app is that you don’t need to wait for a third party developer to implement any ideas you have for the app! I think that’s actually the only nice thing…
Anyway, I’m pretty happy with the results. Next up, I plan on adding a bit of visual polish to the interface and maybe even create an app icon! I also want to aggregate the data like heart rate info, watt effort, etc and keep track of statistics and chart the data, perhaps in real time. I find it very motivating to compare my effort in the same exact workout across different days to see if I’m improving (maybe by measuring heart rate average).
As usual, my changes are in Github in case you happen to have the same exact exercise bike as me or are curious about how I implemented certain things.