Start, stop, and save workouts on Apple Watch with the Workout Builder API.
This sample demonstrates how to create an Apple Watch workout app using the Workout Builder API. The sample displays real-time data, such as heart rate, distance traveled, and elapsed time during an active workout. The user can tap on a button on the initial interface to start the workout, and swipe right on the workout interface to bring up the menu to pause or stop the workout. In the sample, all the business logic of interfacing with HealthKit and managing the workout is encapsulated in the WorkoutManager
object.
To build and run this sample on your device, you must first update the bundle ID in the Info.plist
file in the WatchKit Extension target. Follow these steps to change the bundle ID:
- Open the sample with the latest version of Xcode.
- Select the top-level project.
- For the three targets, select the correct team on the Signing & Capabilities tab (next to Team) to let Xcode automatically manage your provisioning profile.
- Make a note of the Bundle Identifier of the WatchKit App target.
- Open the
Info.plist
file of the WatchKit Extension target, and change the value of theNSExtension > NSExtensionAttributes > WKAppBundleIdentifier
key to the bundle ID you noted in the previous step. - Make a clean build and run the sample app on your device.
Workout apps access the HealthKit data store for real-time data and to save workouts. Apps that use the HealthKit framework should follow the steps in Setting Up HealthKit. In particular, an app must request authorization from the user to access data and save the workout:
// The quantity type to write to the health store.
let typesToShare: Set = [
HKQuantityType.workoutType()
]
// The quantity types to read from the health store.
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .heartRate)!,
HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!
]
// Request authorization for those quantity types.
healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { (success, error) in
// Handle error.
}
View in Source
The sample first creates an HKWorkoutConfiguration
object, and sets its properties to describe the type of activity corresponding to this workout. In the case below, the sample sets the activityType
property to .running
to represent a running workout activity. HealthKit provides constants for dozens of popular workout and fitness activities.
let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
configuration.locationType = .outdoor
View in Source
Next, the sample creates the HKWorkoutSession
and the HKLiveWorkoutBuilder
objects. These two objects are declared as member variables of the WorkoutManager
.
let healthStore = HKHealthStore()
var session: HKWorkoutSession!
var builder: HKLiveWorkoutBuilder!
View in Source
The workout session is required in order to save a workout in the HealthKit store. This initialization throws an exception when the workout configuration parameter is invalid. Then, the sample asks the workout session object for the associated HKLiveWorkoutBuilder
object, which automates the collection of HealthKit quantity types that the sample app displays to the user during the workout.
do {
session = try HKWorkoutSession(healthStore: healthStore, configuration: self.workoutConfiguration())
builder = session.associatedWorkoutBuilder()
} catch {
// Handle any exceptions.
return
}
// Setup session and builder.
session.delegate = self
builder.delegate = self
View in Source
The sample initializes a new HKLiveWorkoutDataSource
object, configured with the same workout configuration object used earlier in creating the workout session. As a result, the data source infers the quantity types to collect. The sample sets the HKLiveWorkoutDataSource
object as the workout builder object's data source.
builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore,
workoutConfiguration: workoutConfiguration())
View in Source
The workout session and workout builder objects are now fully set up, so the sample starts the workout session and the workout builder's data collection.
session.startActivity(with: Date())
builder.beginCollection(withStart: Date()) { (success, error) in
// The workout has started.
}
View in Source
WorkoutManager
is implemented as an ObservableObject
, and publishes the properties that are needed to update the user interface.
@Published var heartrate: Double = 0
@Published var activeCalories: Double = 0
@Published var distance: Double = 0
@Published var elapsedSeconds: Int = 0
View in Source
When HealthKit has new quantities available, it calls the HKLiveWorkoutBuilderDelegate
protocol's workoutBuilder(_:didCollectDataOf:)
method. The sample iterates on the collected quantity types to retrieve the most recent values, then updates the published properties. For example, the sample uses the following process to publish new heart rate values. First, the sample calls the workout builder's' statistics(for:)
method to obtain the HKStatistics
object corresponding to the quantity type in the current iteration.
let statistics = workoutBuilder.statistics(for: quantityType)
View in Source
Then, the sample retrieves the most recent value collected from the HKStatistics
object, rounds it, and publishes the new value by setting the heartrate
published member variable to the new value.
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
self.heartrate = roundedValue
View in Source
To let the user know how long the workout has been in progress, the sample app implements a timer display that shows the elapsed time in seconds. A TimerPublisher
drives the display of the elapsed time. To ensure accuracy, the timer fires at 0.1 second intervals, even though the display shows integer seconds only.
// The cancellable holds the timer publisher.
var start: Date = Date()
var cancellable: Cancellable?
var accumulatedTime: Int = 0
// Set up and start the timer.
func setUpTimer() {
start = Date()
cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
.autoconnect()
.sink { [weak self] _ in
guard let self = self else { return }
self.elapsedSeconds = self.incrementElapsedTime()
}
}
// Calculate the elapsed time.
func incrementElapsedTime() -> Int {
let runningTime: Int = Int(-1 * (self.start.timeIntervalSinceNow))
return self.accumulatedTime + runningTime
}
View in Source
When the user has finished working out, they tap on the End button in the menu. In response, the sample ends the workout session and stops collecting data. The sample calls the workout session's end()
method. HealthKit
will call the workout session delegate's workoutSession(_:didChangeTo:from:date:)
callback method. In this method, the sample calls the workout builder's endCollection(withEnd:completion:)
method to end the collection of data. Then the sample saves the workout along with the associated collected samples and events by calling finishWorkout(completion:)
. In the completion block, the sample resets the workout state.
if toState == .ended {
print("The workout has now ended.")
builder.endCollection(withEnd: Date()) { (success, error) in
self.builder.finishWorkout { (workout, error) in
// Optionally display a workout summary to the user.
self.resetWorkout()
}
}
}
View in Source