Creating a WatchOS app that initiates workout sessions and retrieves heart rate and calories burned is an exciting opportunity to bridge the gap between health tech and software development. With the growing interest in wearable technology and fitness tracking, such a guide provides practical value by teaching developers how to leverage WatchOS-specific APIs like HealthKit and WorkoutKit. It offers a hands-on project that appeals to diverse audiences, from aspiring developers to fitness entrepreneurs, while showcasing real-world applications and fostering innovation in health monitoring. By sharing this knowledge, you not only empower readers to build functional apps but also inspire them to explore new possibilities in the intersection of technology and wellness

Blank watchOS app

We are going to create a blank, ready-to-deploy watchOS app. A very essential point is to have access to an Apple Watch for validating the steps explained in this post. In my case, I am using an Apple Watch Series 9. Now, open Xcode and create a new blank project.

And select ‘watchOS’ and ‘App’. To avoid overloading the sample project with unnecessary elements for the purpose of this post, choose ‘Watch-only App’.
Add signing capabilities for using Healthkit.

The CaloriesBurner target serves as the main wrapper for your project, enabling you to submit it to the App Store. The CaloriesBurner Watch App target is specifically designed for building the watchOS app. This app bundle includes the watchOS app’s code and assets.

Finally, navigate to the Build Settings for the CaloriesBurner Watch App target and set the HealthShareUsageDescription and HealthUpdateUsageDescription fields with appropriate description messages.

We must not forget to prepare the app for Swift 6.0. Set ‘Swift Concurrency Checking’ to ‘Complete’.

… and set Swift Language Version.

Before proceeding with deployment and verifying that the app is displayed properly on a real Apple Watch device, ensure that development mode is enabled on the device.

Request authorization

HealthKit requires explicit user consent to access or share specific types of health information, ensuring users have full control over their data and its usage. It requests user permission to read data such as heart rate and active energy burned, and to write workout data to HealthKit.

    func requestAuthorization() async {
        let typesToShare: Set = [HKObjectType.workoutType()]
        let typesToRead: Set = [HKObjectType.quantityType(forIdentifier: .heartRate)!, HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!]

        do {
            try await healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead)
            internalWorkoutSessionState = .notStarted
        } catch {
            internalWorkoutSessionState = .needsAuthorization
        }
    }

If authorization succeeds, the internal state (internalWorkoutSessionState) is updated to .notStarted, indicating readiness for a workout session.

Ensure that requestAuthorization is called when the view is presented, but only once.

struct ContentView: View {
    @StateObject var healthkitManager = appSingletons.healthkitManager
    var body: some View {
        VStack {
           ...
        }
        .padding()
        .task {
            Task {
                await healthkitManager.requestAuthorization()
            }
        }
    }
}

Build and deploy…

On starting the app, watchOS will request your permission to access the app, your heart rate, and calories burned during workouts.

Workout sample application

Once the app is properly configured and granted all the necessary permissions to request health data, it is ready to start a workout session. To initiate a session, the app provides a button labeled “Start”. When the user presses this button, the session begins, displaying the user’s heart rate and tracking the calories burned in real time.

When the user presses the Start button, the HealthkitManager.startWorkoutSession method is called:

    func startWorkoutSession() async {
        guard session == nil, timer == nil else { return }

        guard HKHealthStore.isHealthDataAvailable() else {
            print("HealthKit is not ready on this device")
            return
        }

        let configuration = HKWorkoutConfiguration()
        configuration.activityType = .running
        configuration.locationType = .outdoor

        do {
            session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
            session?.delegate = self

            builder = session?.associatedWorkoutBuilder()
            builder?.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration)
            builder?.delegate = self
            session?.startActivity(with: Date())

            do {
                try await builder?.beginCollection(at: Date())
            } catch {
                print("Error starting workout collection: \(error.localizedDescription)")
                session?.end()
                internalWorkoutSessionState = .needsAuthorization
            }

            internalWorkoutSessionState = .started
        } catch {
            print("Error creating session or builder: \(error.localizedDescription)")
            session = nil
        }
    }

To retrieve the heart rate and calories burned, the HealthKitManager must implement the HKLiveWorkoutBuilderDelegate protocol.

extension HealthkitManager: HKLiveWorkoutBuilderDelegate {
    nonisolated func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
        print("Workout event collected.")
    }

    func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf types: Set<HKSampleType>) {
        for type in types {
            if let quantityType = type as? HKQuantityType, quantityType == HKQuantityType.quantityType(forIdentifier: .heartRate) {
                handleHeartRateData(from: workoutBuilder)
            }
            if let quantityType = type as? HKQuantityType, quantityType == HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) {
                handleActiveEnergyData(from: workoutBuilder)
            }
        }
    }

    private func handleHeartRateData(from builder: HKLiveWorkoutBuilder) {
        if let statistics = builder.statistics(for: HKQuantityType.quantityType(forIdentifier: .heartRate)!) {
            let heartRateUnit = HKUnit(from: "count/min")
            if let heartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) {
                print("Heart rate: \(heartRate) BPM")
                internalHeartRate = "\(heartRate) BPM"
            }
        }
    }

    private func handleActiveEnergyData(from builder: HKLiveWorkoutBuilder) {
        if let statistics = builder.statistics(for: HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!) {
            let energyUnit = HKUnit.kilocalorie()
            if let activeEnergy = statistics.sumQuantity()?.doubleValue(for: energyUnit) {
                print("Active Energy Burned: \(activeEnergy) kcal")
                internalCaloriesBurned = String(format: "%.2f kcal", activeEnergy)
            }
        }
    }
}

This code defines a HealthkitManager that integrates with Apple HealthKit to track live workout data, specifically heart rate and active energy burned during a workout. It uses the HKLiveWorkoutBuilderDelegate to monitor real-time workout events and data collection. The delegate method workoutBuilder(_:didCollectDataOf:) processes types of health data, focusing on heart rate and active energy burned, which are handled by respective private methods (handleHeartRateData and handleActiveEnergyData). These methods retrieve and print the latest values from HealthKit and store them internally. A workout session is configured for running (outdoor) using HKWorkoutConfiguration, and a live workout builder is initialized to collect data. The session and builder are started with error handling for initialization and data collection failures, and internal states are updated to track the workout’s progress. This setup enables live monitoring and analysis of health metrics during a workout.

For stoping workout session responsible code is folloing:

    func stopWorkoutSession() async {
        guard let session else { return }
        session.end()
        do {
            try await builder?.endCollection(at: Date())
        } catch {
            print("Error on ending data collection: \(error.localizedDescription)")
        }
        do {
            try await builder?.finishWorkout()
        } catch {
            print("Error on ending training: \(error.localizedDescription)")
        }

        internalWorkoutSessionState = .ended
    }

The stopWorkoutSession function is a method that terminates a workout session by first ensuring the session object is non-nil and calling its end method. It then attempts to asynchronously stop data collection (endCollection) and finish the workout (finishWorkout) using the optional builder object, handling any errors in do-catch blocks to log failures without interrupting execution. Finally, it updates the internal state (internalWorkoutSessionState) to .ended, ensuring the session is marked as concluded within the app’s logic. This function manages state, error handling, and asynchronous operations crucial for gracefully ending a workout session in a fitness tracking app.

To see all of this in action, I have prepared the following video:

After requesting permission for having access to health data and press start button, heart beat is presented along with the calories burned since user pressed the button.

Conclusions

In this post, I have demonstrated how to set up a watchOS app and configure HealthKit to display heart rate and calories burned. You can find the source code used for this post in the repository linked below.

References

Copyright © 2024-2025 JaviOS. All rights reserved