Creating experiences for the Apple Watch can be a really fun and rewarding experience, but there are definitely some challenges that come up when developing for a new platform.

Specifically, writing workout apps for the Apple Watch can be a challenge when you consider the requirements of real-time data display, often-unavailable network, and the limited capabilities of the WatchKit framework. On top of all that, try tackling the new swimming APIs when creating the first 3rd-party swimming app for the Watch (aka no stackoverflow help).

Related: Apple Names MySwimPro Best Watch App of the Year

Here I’ll share some of the challenges I encountered along with their solutions.

myswimpro-apple-watch-series-2-swimpro-swim-workout

MySwimPro for Apple Watch

Read more here. Our watch app has two main functions:

  1. Passive workout tracking: Where you just hit “go” and it records how far you swam and at what pace.
  2. Active workout tracking: Load a structured workout from your phone onto the watch and it walks you through the sets.

Why Apple Watch?

There are a number of other wearables we are planning to integrate with. We chose to develop first on the Apple Watch for three key reasons: development expertise, ecosystem, and hype.

I have developed for iOS and watchOS before, and have less experience with other wearable platforms. We also had to do this quick. Leading up to the launch, we had a good feeling that the new Apple Watch would be waterproof, so I developed a prototype in the week leading up to the keynote.

myswimpro-apple-keynote-tim-cook

After it’s announcement (MySwimPro was behind Tim Cook on the screen!), I buckled down to get it production ready. The Apple ecosystem has been much easier to work with. We didn’t need to pay a fee to access it’s API, and the company has been responsive to our needs. Lastly, we wanted to hop on that hype train 🚂.

Water Resistance

Another problem we encountered was the differing levels of water resistance on the Apple Watch line. The Series 0 and 1 were not water resistant to 50 meters, like the Series 2. Typically, with iOS devices, you can set values for UIRequiredDeviceCapabilities that limit installs to devices that have certain hardware, like GPS or a gyroscope. This isn’t available for watchOS, so when we released the app, it could be installed on all devices – and we can’t go telling people to jump into a pool with devices that aren’t water resistant. On top of this, the HealthKit swim tracking abilities and types are only available on Series 2 devices.

apple-watch-pool-swim-myswimpro

Luckily, we have the ability to check the water resistant rating at runtime, with WKInterfaceDevice.current.waterResistanceRating. We leverage this to only show the Profile Stats screen when there is no water resistance rating – so athletes can’t even try to start the workout.

Water Lock

As you may know, capacitive touch screens and water do not play well together – think of your phone screen when it’s wet. Apple’s solution to this is the Water Lock- it ignores all touches on the screen until you spin the digital crown (in which case it unlocks and spits the water out of the speaker). It works pretty well – but it’s not available for developer use. This makes building a working UI extremely difficult.

myswimpro-apple-watch-series-2-water-lock

The only two official options you have for navigation, according to the Human Interface Guide, are hierarchical and page based. Both of these options leave exposed the interactive elements – the back button or side-swiping, respectively – that would be triggered by water on the screen.

The solution we use for this is WKInterfaceController.reloadRootControllersWithNames(). It replaces the root view controller without exposing any of the navigational elements. While it isn’t a pretty solution, it gets the job done.

Displaying live data

This was an unexpected problem (though I should’ve seen it coming): displaying a workout live on-screen is much harder than displaying a static workout. In the case of WatchKit, most of this pain comes from WKInterfaceTimer and restricted access to the main thread.

myswimpro-ui-watch-apple

WKInterfaceTimer has no pause() function because it is completely controlled by Dates. You pass it a Date that it needs to count up from or down to, then call a start() or stop() function. This makes it difficult to pause and resume a workout, where the timer displaying a consistent time is extremely important.

The solution I made was to have a workout session model and view model in addition to the interface controller. The session manager keeps track of things that need to be taken care of from the start to the end: calories, distance swam so far, current set, etc. The view model takes care of everything on the screen for a set: mainly what rep you’re on and (importantly) the adjusted start date that is needed for the WKInterfaceTimer on start and resume. It looks like this:

Watch App Architecture

When the athlete pauses the workout, the view model remembers the date of the pause. When the user wants to resume, the view model adds the difference in time from pauseTime unitl now to cumulativePauseTime, and passes startDate + cumulativePauseTime back to the interface controller to be passed onto WKInterfaceTimer. It looks something like this (in simplified form):

class ActiveViewModel {
var startDate: Date
var cumulativePauseTime: TimeInterval = 0.0
var pauseDate: Date?

// …

func pause() {
pauseDate = Date()
}

func resume() -> Date {
guard let pauseDate = pauseDate else { return Date() }
cumulativePauseTime += pauseDate.timeUntilNow
return startDate.add(cumulativePauseTime)
}
}

The end-to-end lifecycle looks something like this:

Workout Lifecycle

Networking

One of the more obvious challenges to developing for the Apple Watch is its networking capabilities. Since the network and app lifecycle are more unpredictable than on iOS, it’s important to pick the correct ways to transfer data.

There are two main ways to communicate with the watch using the Watch Connectivity framework: WCSession::sendMessage() functions and WCSession::transferUserInfo() functions.

Use sendMessage() when you need to transfer information immediately. We use this when transferring a workout from the phone to the watch. (Another benefit of this is that we don’t need to re-download the workout from the web to the watch.) Not-assured delivery, but it is instant if successful.

Use transferUserInfo() when you need to be assured that the data will make it to the other device. We use this to log the workout – it will automatically queue and send the data after the two devices reconnect. Assured delivery, but not instant.

Cwsvg2HXEAECiKY

Conclusion

Creating a great experience for the Apple Watch is a lot of fun. It’s important to keep in mind a few of the caveats I encountered, a few are summarized below:

  • Problem: Water resistance fragmentation
    • Solution: Have non-swimming functionality (Profile Stats)
  • Problem: Water + Capacitive touch screen = 😕
    • Solution: WKInterfaceController.reloadRootControllers
  • Problem: Live workout tracking with limited UI
    • Solution: Workout session manager with view model for individual sets
  • Problem: Networking
    • Solution: Watch Connectivity + read docs

Leave a Reply

Your email address will not be published. Required fields are marked *