Tracking workouts with HealthKit in iOS apps

Tracking workouts with HealthKit in iOS apps

Learn how to track workouts using HealthKit in a SwiftUI app.

Building a fitness app for iOS presents unique challenges compared to the Apple Watch, which has long been the go-to platform for workout tracking. Apple Watch offers built-in sensors and seamless HealthKit integration, while iPhone and iPad require developers to work around hardware limitations such as the lack of automatic heart-rate monitoring.

Until now, workout tracking through HealthKit has been possible only on Apple Watch. Starting with iOS 26, those same APIs are finally available on iPhone and iPad, with only minor adjustments required for each platform.

In this tutorial, you'll learn how to build a complete workout tracking app for iOS using HealthKit. By the end, you'll have created a functional fitness app that can initiate workout sessions, collect real-time health metrics such as heart rate, calories, and distance, and save workout data to HealthKit.

You'll explore how to set up HealthKit permissions, coordinate workout sessions with multiple HealthKit objects, handle real-time data updates, and build a responsive SwiftUI interface that adapts to different workout states.

Before We Start

To follow this tutorial completely, you should be familiar with:

  • SwiftUI fundamentals: You'll be building the user interface with SwiftUI, so understanding the basics of creating views and layouts, state management, and property wrappers is essential.
  • Async/await in Swift: The tutorial uses Swift concurrency for managing workout operations
  • Basic HealthKit concepts: While we'll explain the specific APIs, familiarity with HealthKit's role in the Apple ecosystem will be helpful.

Requirements:

  • Xcode 26 or later;
  • A physical iOS device running iOS 26 (HealthKit doesn't work in the Simulator);
  • An optional Bluetooth heart rate monitor for testing heart rate functionality.

You don't need a starter project for this tutorial; we'll build everything from scratch. However, you will need to create a new iOS App project in Xcode before beginning.

Step 1 - Setting Up HealthKit Permissions

Before diving into workout tracking, you need to configure your project to request proper HealthKit permissions from the user. HealthKit requires explicit authorization to both read existing health data and write new workout sessions to the user's health database.

First, add the HealthKit capability to your Xcode project by going to your target's Signing & Capabilities tab and clicking the "+ Capability" button, then selecting HealthKit.

Next, add these two entries to your Info.plist file:

  • Privacy - Health Share Usage Description, adding a String like: "This app needs access to read your health data to display workout metrics during your exercise sessions."
  • Privacy - Health Update Usage Description, with a String like: "This app needs permission to save your workout data to HealthKit so it can be shared with other health apps."

Now, let’s create the WorkoutManager class, this will serve as the central structure throughout the tutorial. It will handle HealthKit permissions, manage workout sessions, and implement the countdown logic. In other words, most of the app’s core functionality will live here.

Let’s start by adding a method to request authorization:

import HealthKit
import Foundation

class WorkoutManager: NSObject, ObservableObject {

    // Request permission to read and write health data
    private func requestHealthKitPermission() {
        // 1.
        guard
            let hr = HKObjectType.quantityType(forIdentifier: .heartRate),
            let kcal = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
            let dist = HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)
        else {
            return
        }
    
        // 2.
        let read: Set = [hr, kcal, dist, HKObjectType.workoutType()]
        let write: Set = [kcal, dist, HKObjectType.workoutType()]
    
        // 3.
        healthStore.requestAuthorization(toShare: write, read: read) { success, error in
            if let error {
                print("HealthKit auth error:", error.localizedDescription)
            } else {
                print(success ? "HealthKit access granted" : "HealthKit access denied")
            }
        }
    }

}

WorkoutManager.swift

  1. Create quantity type objects for the specific health metrics you want to track: heart rate, active energy (calories), and walking/running distance. The guard statement safely unwraps these optional types.
  2. Define two sets of permissions: read permissions allow your app to display existing health data and workout history, while write permissions let you save new workout sessions and metrics to HealthKit's centralized database.
  3. Request authorization from the HealthKit store with both read and write permissions. The completion handler indicates whether the user granted access or if any errors occurred.

Step 2 - Understanding the Workout Architecture

Before implementing workout tracking, it's important to understand how HealthKit coordinates workout sessions. A workout session in HealthKit is managed by four main objects that work together:

  • HKWorkoutConfiguration: defines the type of the workout (running, cycling, swimming) and the environment (indoor, outdoor)
  • HKWorkoutSession: manages the workout's lifecycle (starting, pausing, resuming and ending)
  • HKLiveWorkoutBuilder: collects and saves workout data in real-time
  • HKLiveWorkoutDataSource: pulls information from sensors so you don’t have to manage them manually

In the WorkoutManager class, now declare the following properties:

import HealthKit
import Foundation

class WorkoutManager: NSObject, ObservableObject {
    // 1.
    private let healthStore = HKHealthStore()
    
    // 2.
    private var workoutSession: HKWorkoutSession?
    private var workoutBuilder: HKLiveWorkoutBuilder?
    
    // 3.
    @Published var currentState: WorkoutState = .idle
    @Published var metrics = WorkoutMetrics()
    
    // 4.
    private var startDate: Date?
    private var elapsedTimeTimer: Timer?

    private func requestHealthKitPermission() { ... }
}

WorkoutManager.swift

  1. Create the HKHealthStore instance, which serves as the main interface to HealthKit for all health data operations.
  2. Declare optional properties for the workout session and builder, which will be initialized when a workout starts.
  3. Use @Published property wrappers to automatically notify SwiftUI views when the workout state or metrics change, enabling reactive UI updates.
  4. Track the workout start time and a timer for updating elapsed time in the UI.

Then create two separate Swift files to hold the metrics and state logic:

// 5.
// Defines the different states a workout can be in
import Foundation

enum WorkoutState {
    case idle       // No workout is running
    case preparing  // Workout is being set up
    case active     // Workout is currently running
    case paused     // Workout is temporarily stopped
    case finished   // Workout has been completed and saved
}

WorkoutState.swift

// 6.
// Data model to hold all workout metrics in one place
import Foundation 

struct WorkoutMetrics {
    var elapsedTime: TimeInterval = 0
    var heartRate: Double = 0
    var isHeartRateAvailable: Bool = false
    var totalCalories: Double = 0
    var totalDistance: Double = 0
    
    mutating func reset() {
        elapsedTime = 0
        heartRate = 0
        isHeartRateAvailable = false
        totalCalories = 0
        totalDistance = 0
    }
}

WorkoutMetrics.swift

  1. Define an enum to represent all possible workout states, helping manage UI presentation and prevent invalid operations.
  2. Create a struct to hold all real-time workout metrics that will be displayed in the UI, with a reset() method to clear data between workouts.

Step 3 - Starting a Workout Session

Now you'll implement the complete workflow for starting a workout, which involves creating the configuration, initializing the session, preparing sensors, and beginning data collection.

Add this method to your WorkoutManager class:

// Starts a new workout session with the specified activity type
func startWorkout(activity: HKWorkoutActivityType = .running, location: HKWorkoutSessionLocationType = .outdoor) async {
    
    // 1.
    metrics.reset()
    currentState = .preparing
    
    do {
        // 2.
        let workoutConfig = HKWorkoutConfiguration()
        workoutConfig.activityType = activity
        workoutConfig.locationType = location
        
        // 3.
        let workoutInstance = try HKWorkoutSession(healthStore: healthStore, configuration: workoutConfig)
        workoutInstance.delegate = self
        
        // 4.
        let dataBuilder = workoutInstance.associatedWorkoutBuilder()
        dataBuilder.delegate = self
        
        // 5.
        dataBuilder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: workoutConfig)
        
        // 6.
        workoutSession = workoutInstance
        workoutBuilder = dataBuilder
        
        // 7.
        workoutInstance.prepare()
        
        // 8.
        await showCountdown()
        
        // 9.
        let workoutStartTime = Date()
        startDate = workoutStartTime
        workoutInstance.startActivity(with: workoutStartTime)
        
        // 10.
        try await dataBuilder.beginCollection(at: workoutStartTime)
        
        // 11.
        currentState = .active
        startElapsedTimeTimer()
        
        print("Workout started successfully")
        
    } catch {
        print("Failed to start workout: \(error.localizedDescription)")
        currentState = .idle
    }
}

WorkoutManager.swift

  1. Reset any metrics from a previous workout and update the state to preparing to show the user that initialization is in progress.
  2. Create the workout configuration that defines what type of activity this is (running, cycling, walking) and where it's happening (indoor or outdoor).
  3. Initialize the workout session with the health store and configuration, and set this class as the delegate to receive session state updates.
  4. Get the associated workout builder from the session, which is responsible for collecting and saving all health data, and set this class as its delegate to receive data updates.
  5. Attach a live data source to the builder, which automatically monitors sensors and external devices (like Bluetooth heart rate monitors) and feeds data to the builder.
  6. Store references to both the session and builder as instance properties so they can be accessed throughout the workout lifecycle.
  7. Call prepare() to activate sensors and allow external devices to connect, ensuring you don't miss early workout data.
  8. Show a brief countdown to give sensors time to initialize and provide a better user experience.
  9. Record the start time and officially begin the workout activity, which signals to HealthKit and the system that a workout is in progress.
  10. Tell the builder to start accumulating health data samples from this point forward.
  11. Update the state to active and start a timer to track elapsed time for the UI.

Step 4 - Implementing the Countdown

The countdown gives HealthKit a moment to initialize sensors before data collection begins, preventing missed data points at the start of the workout.

Now, add this method to your WorkoutManager class:

// Shows a 3-second countdown to give sensors time to activate
private func performCountdown() async {
    // 1.
    for i in (1...3).reversed() {
        print("Starting in \(i)...")
        // 2.
        try? await Task.sleep(nanoseconds: 1_000_000_000)
    }
    print("Workout started!")
}

WorkoutManager.swift

  1. Loop through numbers 3, 2, 1 in reverse order to create a countdown effect.
  2. Use Swift's async Task.sleep to pause for one second (1 billion nanoseconds) between each number, giving sensors time to warm up.

You'll also need to implement the timer for tracking elapsed time:

// 3.
private func startElapsedTimeTimer() {
    elapsedTimeTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
        guard let self = self, let startDate = self.startDate else { return }
        self.metrics.elapsedTime = Date().timeIntervalSince(startDate)
    }
}

// 4.
private func stopElapsedTimeTimer() {
    elapsedTimeTimer?.invalidate()
    elapsedTimeTimer = nil
}

WorkoutManager.swift

  1. Create a timer that fires every second to calculate and update the elapsed time based on the difference between the current time and the workout start time.
  2. Stop and clean up the timer when the workout is paused or ended.

Step 5 - Managing Workout States

Your app needs to handle pausing, resuming, and ending workouts. Each state transition requires coordinating with HealthKit and updating the UI accordingly.

Add these methods to your WorkoutManager class:

// 1.
func pauseWorkout() {
    workoutSession?.pause()
    currentState = .paused
    stopElapsedTimeTimer()
    print("Workout paused")
}

// 2.
func resumeWorkout() {
    workoutSession?.resume()
    currentState = .active
    startElapsedTimeTimer()
    print("Workout resumed")
}

// 3.
func endWorkout() {
    guard let session = workoutSession else { return }
    session.stopActivity(with: Date())
    stopElapsedTimeTimer()
    print("Ending workout...")
}

WorkoutManager.swift

  1. Pause the workout by calling pause() on the session, which temporarily stops data collection while preserving all accumulated metrics, then stop the UI timer.
  2. Resume the workout by calling resume() on the session to continue data collection, and restart the elapsed time timer.
  3. End the workout by calling stopActivity(), which allows final metrics to be collected before the session fully stops.

Step 6 - Handling Real-Time Data Updates

As the workout progresses, HealthKit continuously delivers new health data samples. You need to process these updates to display real-time metrics in your UI.

Implement the method that processes each type of health data:

// Updates the UI with new health statistics
private func updateMetrics(for statistics: HKStatistics) {
    // 1.
    switch statistics.quantityType {
        
    // 2.
    case HKQuantityType.quantityType(forIdentifier: .heartRate):
        if let heartRateValue = statistics.mostRecentQuantity()?.doubleValue(for: .count().unitDivided(by: .minute())) {
            metrics.heartRate = heartRateValue
            metrics.isHeartRateAvailable = true
        }
        
    // 3.
    case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
        if let caloriesValue = statistics.sumQuantity()?.doubleValue(for: .kilocalorie()) {
            metrics.totalCalories = caloriesValue
        }
        
    // 4.
    case HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning):
        if let distanceValue = statistics.sumQuantity()?.doubleValue(for: .meter()) {
            metrics.totalDistance = distanceValue
        }
        
    default:
        break
    }
}
  1. Use a switch statement to handle each type of health data differently based on its quantity type identifier.
  2. For heart rate, use mostRecentQuantity() to get the current reading in beats per minute, and mark heart rate as available so the UI knows to display it.
  3. For calories, use sumQuantity() to get the cumulative total energy burned throughout the workout in kilocalories.
  4. For distance, use sumQuantity() to get the total distance traveled in meters, which can be converted to kilometers in the UI.

Now, implement the HKLiveWorkoutBuilderDelegate extension to WorkoutManager to receive data updates:

extension WorkoutManager: HKLiveWorkoutBuilderDelegate {
    
    // 1.
    func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, 
                       didCollectDataOf collectedTypes: Set<HKSampleType>) {
        
        // 2.
        for sampleType in collectedTypes {
            guard let quantityType = sampleType as? HKQuantityType else { continue }
            
            // 3.
            let statistics = workoutBuilder.statistics(for: quantityType)
            
            // 4.
            if let stats = statistics {
                updateMetrics(for: stats)
            }
        }
    }
}

WorkoutManager.swift

  1. This delegate method is called whenever new health data is collected during the workout, providing you with a set of sample types that have new data available.
  2. Iterate through each collected sample type and filter for quantity types, which represent numeric health data like heart rate, calories, and distance.
  3. Retrieve the latest statistics for this quantity type from the workout builder, which aggregates all samples into useful metrics.
  4. If statistics are available, pass them to the update method to process and display in the UI.

Step 7 - Saving the Workout to HealthKit

When a workout ends, you need to finalize data collection and save the complete workout to HealthKit. This happens through the workout session delegate.

Implement the HKWorkoutSessionDelegate extension to WorkoutManager:

extension WorkoutManager: HKWorkoutSessionDelegate {
    
    func workoutSession(_ workoutSession: HKWorkoutSession,
                       didChangeTo toState: HKWorkoutSessionState,
                       from fromState: HKWorkoutSessionState,
                       date: Date) {
        // 1.
        if toState == .stopped,
           let builder = workoutBuilder {
            Task {
                do {
                    // 2.
                    try await builder.endCollection(at: date)
                    
                    // 3.
                    let savedWorkout = try await builder.finishWorkout()
                    
                    // 4.
                    workoutSession.end()
                    
                    // 5.
                    await MainActor.run {
                        currentState = .finished
                        print("Workout saved to HealthKit: \(savedWorkout)")
                    }
                } catch {
                    print("Failed to save workout: \(error.localizedDescription)")
                    await MainActor.run {
                        currentState = .idle
                    }
                }
            }
        }
    }
    
    // 6.
    func workoutSession(_ workoutSession: HKWorkoutSession, 
                       didFailWithError error: Error) {
        print("Workout session failed: \(error.localizedDescription)")
    }
}
  1. This delegate method is called whenever the workout session changes state. We specifically handle the transition to the stopped state, which occurs after stopActivity() is called.
  2. Stop collecting workout data at the specified end time, which finalizes all accumulated metrics.
  3. Call finishWorkout() to process all collected data and save the complete workout to HealthKit's database, making it available to other health apps.
  4. End the workout session, releasing any system resources and external device connections.
  5. Update the UI state to finished on the main actor (main thread) to show the workout completion screen.
  6. Implement the error delegate method to handle any workout session failures gracefully.

Step 8 - Building the SwiftUI Interface

Now you'll create a responsive SwiftUI interface that displays real-time workout metrics and adapts to different workout states.

Create a new SwiftUI view file:

import SwiftUI

struct WorkoutView: View {
    // 1.
    // Connect to our workout manager to get real-time updates
    @StateObject private var workoutManager = WorkoutManager()
    
    var body: some View {
        NavigationView {
            VStack(spacing: 30) {
                
                // 2.
                Text("Workout Tracker")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                
                // 3.
                VStack(spacing: 20) {
                    
                    MetricCard(
                        title: "Duration",
                        value: formatTime(workoutManager.metrics.elapsedTime),
                        unit: "",
                        color: .blue
                    )
                    
                    // 4.
                    if workoutManager.metrics.isHeartRateAvailable {
                        MetricCard(
                            title: "Heart Rate",
                            value: String(format: "%.0f", workoutManager.metrics.heartRate),
                            unit: "BPM",
                            color: .red
                        )
                    } else if workoutManager.currentState == .active {
                        VStack {
                            Image(systemName: "heart.slash")
                                .font(.title)
                                .foregroundColor(.orange)
                            Text("Connect a heart rate monitor")
                                .font(.caption)
                                .foregroundColor(.orange)
                        }
                        .padding()
                        .background(Color.orange.opacity(0.1))
                        .cornerRadius(10)
                    }
                    
                    MetricCard(
                        title: "Calories",
                        value: String(format: "%.0f", workoutManager.metrics.totalCalories),
                        unit: "kcal",
                        color: .orange
                    )
                    
                    MetricCard(
                        title: "Distance",
                        value: String(format: "%.2f", workoutManager.metrics.totalDistance / 1000),
                        unit: "km",
                        color: .green
                    )
                }
                .padding(.horizontal)
                
                // 5.
                VStack(spacing: 15) {
                    switch workoutManager.currentState {
                        
                    case .idle:
                        Button("Start Running Workout") {
                            Task {
                                await workoutManager.startWorkout(activity: .running, location: .outdoor)
                            }
                        }
                        .buttonStyle(StartButtonStyle())
                        
                    case .preparing:
                        VStack {
                            ProgressView()
                                .scaleEffect(1.2)
                            Text("Preparing workout...")
                                .font(.headline)
                                .foregroundColor(.blue)
                        }
                        
                    case .active:
                        HStack(spacing: 20) {
                            Button("Pause") {
                                workoutManager.pauseWorkout()
                            }
                            .buttonStyle(PauseButtonStyle())
                            
                            Button("End Workout") {
                                workoutManager.endWorkout()
                            }
                            .buttonStyle(EndButtonStyle())
                        }
                        
                    case .paused:
                        HStack(spacing: 20) {
                            Button("Resume") {
                                workoutManager.resumeWorkout()
                            }
                            .buttonStyle(ResumeButtonStyle())
                            
                            Button("End Workout") {
                                workoutManager.endWorkout()
                            }
                            .buttonStyle(EndButtonStyle())
                        }
                        
                    case .finished:
                        VStack(spacing: 15) {
                            VStack {
                                Image(systemName: "checkmark.circle.fill")
                                    .font(.title)
                                    .foregroundColor(.green)
                                Text("Workout Complete!")
                                    .font(.title2)
                                    .fontWeight(.semibold)
                                    .foregroundColor(.green)
                                Text("Your workout has been saved to HealthKit")
                                    .font(.caption)
                                    .foregroundColor(.secondary)
                            }
                            
                            Button("Start New Workout") {
                                workoutManager.currentState = .idle
                                workoutManager.metrics.reset()
                            }
                            .buttonStyle(StartButtonStyle())
                        }
                    }
                }
                
                Spacer()
            }
            .padding()
            .navigationBarHidden(true)
        }
    }
    
    // 6.
    private func formatTime(_ timeInterval: TimeInterval) -> String {
        let hours = Int(timeInterval) / 3600
        let minutes = (Int(timeInterval) % 3600) / 60
        let seconds = Int(timeInterval) % 60
        
        if hours > 0 {
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
        } else {
            return String(format: "%d:%02d", minutes, seconds)
        }
    }
}

WorkoutView.swift

  1. Use @StateObject to create and manage the workout manager instance, which automatically updates the UI whenever published properties change.
  2. Display the app title at the top of the screen.
  3. Create a vertical stack of metric cards showing duration, heart rate, calories, and distance in real-time.
  4. Conditionally display heart rate data if available, or show a message prompting users to connect an external heart rate monitor when a workout is active but no sensor is detected.
  5. Use a switch statement on the current workout state to display appropriate controls: a start button when idle, a progress indicator when preparing, pause/end buttons when active, resume/end buttons when paused, and a completion message with a reset button when finished.
  6. Create a helper function to format elapsed time in HH:MM:SS or MM:SS format depending on duration.

Step 9 - Creating Reusable Components

To keep your UI code clean and maintainable, create a reusable component for displaying workout metrics.

Add this new view to your project:

import SwiftUI

// A card that displays a workout metric with title, value, and unit
struct MetricCard: View {
    let title: String
    let value: String
    let unit: String
    let color: Color
    
    var body: some View {
        VStack(spacing: 8) {
            // 1.
            Text(title)
                .font(.headline)
                .foregroundColor(.secondary)
            
            // 2.
            HStack(alignment: .lastTextBaseline, spacing: 4) {
                Text(value)
                    .font(.system(size: 40, weight: .bold, design: .rounded))
                    .foregroundColor(color)
                
                // 3.
                if !unit.isEmpty {
                    Text(unit)
                        .font(.title3)
                        .foregroundColor(.secondary)
                }
            }
        }
        .frame(maxWidth: .infinity)
        .padding()
        .background(Color(uiColor: .systemGray6))
        .cornerRadius(12)
    }
}

// 4.
struct StartButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.headline)
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(Color.green)
            .cornerRadius(12)
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
    }
}

// 5.
struct PauseButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.headline)
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(Color.orange)
            .cornerRadius(12)
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
    }
}

// 6.
struct ResumeButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.headline)
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(Color.blue)
            .cornerRadius(12)
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
    }
}

// 7.
struct EndButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.headline)
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(Color.red)
            .cornerRadius(12)
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
    }
}

MetricCard.swift

  1. Display the metric title (Duration, Heart Rate, Calories, Distance) in a smaller secondary font.
  2. Show the metric value in a large, bold, rounded font with a color that matches the metric type.
  3. Display the unit of measurement (BPM, kcal, km) in a smaller font next to the value, but only if a unit string was provided.
  4. Create a green start button style that spans the full width and slightly scales down when pressed for tactile feedback.
  5. Create an orange pause button style with similar press feedback.
  6. Create a blue resume button style to indicate continuing a paused workout.
  7. Create a red end button style to clearly communicate the destructive action of ending the workout.

Final Result

Congratulations! You've successfully built a complete workout tracking app for iOS using HealthKit. Your app can now start workout sessions, collect real-time health metrics (including heart rate, calories burned, and distance traveled) and save complete workouts to HealthKit's database.

Throughout this tutorial, you explored the core HealthKit workout APIs:

  • HKWorkoutConfiguration for defining workout parameters
  • HKWorkoutSession for managing the workout lifecycle
  • HKLiveWorkoutBuilder for collecting and saving data
  • HKLiveWorkoutDataSource for automatic sensor management

You also built a responsive SwiftUI interface that adapts to different workout states and handles the absence of external heart rate monitors gracefully.

0:00
/0:29

You can download the project to review the full implementation or use it as a reference for your own fitness apps.