Making an iOS Zwift Clone to Save $15 a Month! Part 2: Reverse Engineering a Workout
Last time, on “Making an iOS Zwift Clone to Save $15 a Month” I wrote about learning Core Bluetooth to connect to my exercise bike and get data streaming directly to my app.
Since writing that article, I cleaned up the implementation of the Core Bluetooth service a bit and started supporting some additional data like distance, calories burned and cycling cadence.
While cycling on my exercise bike and staring at these numbers is fun, the built-in screen on my bike already shows these numbers, so I essentially recreated a subset of the official ergData app so far.
I realized the next challenge would be to start a guided workout in my app and show the target wattage alongside my actual wattage on the bike.
Enter the Workout
Zwift workouts seem pretty simple to begin with. You can set up a number of workout segments to guide your workout. There are warmups, steady states, intervals, free ride sections and cool-downs. At any given point in your workout, Zwift will tell you to put in a certain amount of effort. You just try to get your watt number to match what Zwift wants you to do.
At first I figured I could come up with my own way of representing workouts, but that would also require me to re-create all of the workouts I usually use on Zwift. Zwift also has a nice workout editor that can use to edit preset workouts or create completely new ones on your own.
I discovered that if you copy an existing workout to edit it, the workout will be saved as user data and will be synced between all devices. How is the workout data saved, you ask? No, not as JSON as a sane developer would use. It’s XML. Okay, so technically it’s a .zwo file but I know what XML looks like! Here’s a sample of an exported workout file:
So I went ahead and implemented an XMLParser in Swift to turn .zwo files into my own Workout struct. I won’t bore you with the implementation details but it was kind of a walk down memory lane dealing with the delegation-based API of XMLParser. I’m surprised there isn’t a more modern solution in Foundation but I’m guessing it’s because most people have moved on from XML.
Let’s Reverse Engineering
By reading the .zwo files, I was able to figure out how Zwift represents their workouts and basically reverse engineer the structure of a workout:
- Warmups have a duration, a low and high power (which is a percentage of FTP). The warmup begins at the low power and steadily ramps up to the high power by the end of the segment.
- SteadyStates are the simplest, with just a duration and power.
- Intervals are the most complicated, with a repeat number, onDuration, offDuration, onPower and offPower. The intervals segment goes between the high and low power levels, and repeats a certain number of times. I didn’t want to deal with the extra complexity of this type so I just wrote some code to expand them out into SteadyStates.
- Cooldowns are like warmups but in reverse.
There are also some other things like text captions that appear during a workout, free ride segments and cadence settings that I didn’t bother to implement because I don’t use them. The cadence stuff makes sense if you’re using a smart trainer but my bike doesn’t support automatically changing resistance anyway.
I decided to represent the WorkoutSegment as a Swift enum since it needs to support a bunch of different formats with varying levels of complexity. I’m actually kinda curious how the Zwift team ended up representing these and if they did something similar.
From here it’s pretty simple to get stuff like the total duration or the target wattage for a particular time offset, whether I need to calculate that (in the case of a warmup or cooldown) or if I just return the constant value.
Aside from representing the workout segments, the actual workout object will consist of things like the name, description, FTP and other information that pertains to the state of the workout like time elapsed. I wrote a function that takes the current time elapsed in the workout, loops through and finds the corresponding segment and returns the desired wattage. There might be a more efficient way to do that but for now it works.
I also added some things to my project for displaying and choosing workouts from a list, and actually displaying the target wattage alongside actual wattage. I want to improve the interface now since it’s terrible, but I’ll save that for another blog post. If you want to check out the project, it’s all here on Github, including the playgrounds I used to test out the workout segment logic before adding it to the Xcode project.
Next on my list of tasks is to make the workout interface a lot prettier/usable, and to add heart rate information from my Apple Watch.