Deliver workout insights with HealthKit workout zones
Use HealthKit workout zones in iOS 27 and watchOS 27 to read completed workout zone data, handle live zone changes, and configure preferred or custom zones.
TL;DR
- HealthKit now supports heart rate and cycling power workout zones, automatically calculating time-in-zone from workout samples when users authorize the relevant data types.
- Completed workouts expose zone data through
zoneGroupsByTypeonHKWorkoutorHKWorkoutActivity, including the zone configuration, source, boundaries, and per-zone durations. - Live workouts can receive zone-change updates through
HKLiveWorkoutBuilderDelegate.didUpdateWorkoutZone, enabling active-zone UI, alerts, and live coaching. - Apps can use preferred zone thresholds from Health Settings or provide a custom
HKWorkoutZoneConfigurationbeforebeginCollection; custom configurations are workout-scoped and must be normalized carefully when comparing workouts.
Workout zones in HealthKit
Workout zones turn raw metrics such as heart rate samples or cycling power into intensity ranges that are easier to use for training guidance. Heart rate zones are personalized using factors such as age and resting heart rate, while cycling power zones are based on functional threshold power.
In iOS 27 and watchOS 27, HealthKit integrates heart rate and cycling power zones directly. HealthKit calculates time spent in each zone from incoming workout samples, letting apps build post-workout summaries, live coaching, and training dashboards without implementing the core zone accounting themselves.
- Request HealthKit authorization for the relevant data before accessing zone information, such as workouts, heart rate, and cycling power.
- Heart rate and cycling power zones share a similar HealthKit structure; switch the
HKQuantityTypeto retrieve the metric you need. - Zones are useful for classifying effort, tracking whether a workout stayed within a target intensity, and balancing recovery or load.
Read zones from completed workouts
After a workout finishes, zone data is available from the zoneGroupsByType dictionary on either an HKWorkout or an individual HKWorkoutActivity. This supports both whole-workout summaries and per-activity views for multi-sport workouts.
An HKWorkoutZoneGroup contains a configuration and zoneDurations. The configuration describes the quantity type, the source of the thresholds, and the ordered zones. Zone boundaries are contiguous and non-overlapping; the first zone has no lower bound and the last has no upper bound. The durations array gives the time spent in each zone, ordered by threshold values.
- Use
HKQuantityType(.heartRate)for heart rate zones or the cycling power quantity type for cycling power zones. - Use
zoneDurationsto render time-in-zone charts or classify the completed workout's intensity. - Inspect the configuration source to understand whether thresholds came from the system, the user, or the app.
Reading heart rate zones from a completed workout
Looks up the completed workout's heart rate zone group and converts its zone count and durations into app display data.
if let heartRateZoneGroup = workout.zoneGroupsByType?[HKQuantityType(.heartRate)] {
let zones = ZoneDisplayData(
zoneCount: heartRateZoneGroup.configuration.zones.count,
currentZoneIndex: nil,
durations: heartRateZoneGroup.zoneDurations.map(\.duration)
)
}Handle live zone updates
During a live workout, HealthKit processes incoming samples and determines the current zone. When the current zone changes, HealthKit sends an update to the workout builder delegate.
Use HKLiveWorkoutBuilderDelegate and implement didUpdateWorkoutZone to respond to zone transitions. Each update includes the current and previous zones, the full zone group with cumulative totals, and a timestamp for the last processed sample. The timestamp can support a running timer for time in the current zone.
- Live updates are sent on zone changes, such as moving from Zone 2 to Zone 3.
- Use the current zone to highlight UI, guide pacing, or notify someone when they leave a target zone.
- Use the cumulative zone group to keep live time-in-zone totals in sync with HealthKit.
Handling live zone updates
Responds to HealthKit live zone changes by rebuilding display data and updating UI state on the main actor.
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder,
didUpdateWorkoutZone zoneUpdate: HKLiveWorkoutZoneUpdate) {
guard let zoneGroup = zoneUpdate.zoneGroup else {
return
}
if let currentIndex = zoneUpdate.currentZoneDuration?.zone.index {
let data = ZoneDisplayData(
zoneCount: zoneGroup.configuration.zones.count,
currentZoneIndex: currentIndex,
durations: zoneGroup.zoneDurations.map(\.duration)
)
Task { @MainActor in
self.heartRateZones = data
}
}
}Preferred zones from Health Settings
By default, HealthKit uses preferred workout zone thresholds from Health Settings. These can be calculated automatically by the system from available user metrics or manually configured by the user. Preferred zones sync across devices through HealthKit, giving users a consistent experience across apps.
Before starting a workout that depends on zone information, query for a preferred zone configuration on HKHealthStore or HKWorkoutBuilder to confirm one exists.
- Preferred zone sources may be system-calculated or manually set by the user.
- Use preferred zones when your app does not need a proprietary zone model.
- Checking for a preferred configuration lets your app decide whether to proceed, prompt the user, or supply a workout-specific custom configuration.
Check whether a preferred heart rate zone configuration exists
Queries the workout builder for the preferred heart rate zone configuration before starting collection.
if try await builder.zoneConfiguration(for: HKQuantityType(.heartRate)) == nil {
// Provide a custom configuration or handle the missing preference.
}Custom workout zone configurations
Use custom zones when your app's training model differs from the user's preferred Health Settings zones, such as a proprietary coaching platform. Create an HKWorkoutZoneConfiguration from a quantity type and compatible boundary quantities, then set it on the HKWorkoutBuilder before collection begins.
Custom zone configurations are scoped to the individual workout and are not persisted by HealthKit. If your app needs to reuse or sync them, it is responsible for storing and syncing that configuration.
HealthKit requires between 3 and 9 zones. Because different workouts may have different zone counts and thresholds, do not compare zone indexes directly across workouts without considering their boundaries.
- Boundary units must match and be compatible with the configuration's
HKQuantityType. - The first zone starts at 0 and the final zone is unbounded.
- Call
setCustomZoneConfiguration(_:for:)beforebeginCollection(at:). - When comparing time-in-zone across workouts with different zone definitions, normalize using the original samples and the target bucket definitions rather than assuming, for example, Zone 3 means the same thing everywhere.
Create heart rate zone boundaries and configuration
Builds a heart rate zone configuration from beats-per-minute thresholds.
let defaultHeartRateZoneThresholds = [91.0, 114.0, 136.0, 158.0]
let bpmUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
let boundaries = defaultHeartRateZoneThresholds.map {
HKQuantity(unit: bpmUnit, doubleValue: $0)
}
let heartRate = HKQuantityType(.heartRate)
let defaultConfiguration = try HKWorkoutZoneConfiguration(
quantityType: heartRate,
zoneBoundaries: boundaries
)Set a custom configuration before collection
Applies the custom zone configuration to the workout builder before starting HealthKit data collection.
try await builder.setCustomZoneConfiguration(defaultConfiguration, for: heartRate)
let startDate = Date()
try await builder.beginCollection(at: startDate)