iOS 18 and watchOS 11 introduced a new metrics to allow users to record the effort for a workout: a score between 1 and 10 rates the workout from easy to all-out challenging. This metrics contributes to the user’s overall Training Load, as can be seen on a chart in the Fitness app.

Naturally, I want my weight training app Liiift to also allow my users to record that important information. The problem is how: documentation is scarce, and I had to put together this from a few Apple’s official pages that only implied the API calls, as well as a helpful Stackoverflow answer that assembled some of the code. I hope this can help you if you’re looking to do the same!

Asking for Permissions

As with all HealthKit data, you need to request permissions before you’ll be able to read and write (“share”) records. You can add workoutEffortScore object type to the permission request. This is how I do it, keeping in mind my app supports iOS 16+:

let typesToShare: Set<HKSampleType> = {
	var types: Set<HKSampleType> = [
		HKQuantityType.workoutType(),
	]
	if #available(iOS 18.0, watchOS 11.0, *) {
		types.insert(HKQuantityType(.workoutEffortScore))
	}
	return types
}()

I’ve also done something similar to my types-to-read variable, including workoutEffortScore when the app runs on iOS 18 or watchOS 11.

Note that there is also an estimatedWorkoutEffortScore object type. Per my testing, Apple’s Fitness app may display the score if and only if no record of workoutEffortScore (non-estimated type) is available. When the user rates the workout manually in Fitness, the new written record is indeed workoutEffortScore. This is how they are different:

Reading from HealthKit

To read the workout effort score efforts with Concurrency, use HKSampleQueryDescriptor. Let’s also assume you have queried and fetched the HKWorkout object from HealthKit. Recall that an HKWorkout object is akin to a container that associates many other types of HealthKit records (most commonly, workout minutes and active calories). Workout effort scores are similar: they are also separate HealthKit records and get attached to the workout; the only difference is that an effort score entry makes no sense standing on its own.

To query the records, you’ll need to set the sample type to HKQuantityType(.workoutEffortScore) and the predicate to HKQuery.predicateForWorkoutEffortSamplesRelated(workout: workout, activity: nil)

Put together, this is the code:

@MainActor
@available(iOS 18.0, *)
func effortScore(for workout: HKWorkout) async throws -> Int? {
	let sampleType = HKQuantityType(.workoutEffortScore)
	let predicate = HKQuery.predicateForWorkoutEffortSamplesRelated(workout: workout, activity: nil)
	let descriptor = HKSampleQueryDescriptor(predicates: [
		HKSamplePredicate.sample(type: sampleType, predicate: predicate)
	], sortDescriptors: [])
	
	let samples = try await descriptor.result(for: healthStore)
	
	if let firstSample = samples.last as? HKQuantitySample {
		let doubleValue = firstSample.quantity.doubleValue(for: .appleEffortScore())
		return Int(doubleValue)
	} else {
		return nil
	}
}

A question may come to you naturally: there can be multiple effort score entries associated to a workout. Notably, Apple’s Fitness app seems to write a new record every time the user updates it. How do we find which one to use?

In my code above, you’ll notice there’s no sort descriptor specified. Ideally, we should be finding the effort score entry that was last updated/modified/created, but there’s no public API for us to achieve this.

In my testing, however, when I only use the last entry from the fetched list of records, it’s always the most recent record. For now, I’ll stick to this method, but if you know how to sort the records, please let me know!

Finally, I’ve tried to call the HKWorkout object’s statistics(for:) method to try to fetch the score. Well… it just doesn’t work! But again, please let me know if I’m wrong. This is the code I’ve tried but quickly scrapped from my app:

let sampleType = HKQuantityType(.workoutEffortScore)
if let doubleValue = workout.statistics(for: sampleType)?.mostRecentQuantity()?.doubleValue(for: .appleEffortScore()) {
	return Int(doubleValue)
} else {
	return nil
}

Ideally, this should be the way to go, the same way we find a workout’s total calories: it both helps define the most recent record via an official, public API, and allows us to fetch the value in one synchronous call. I’m not sure what led to the decision to not support this 🤷

Writing to HealthKit

Writing to HealthKit follows a similar pattern, except that there’s a new HKHealthStore method to “relate” the effort score record to a workout:

@MainActor
@available(iOS 18.0, *)
func updateEffortScore(_ newScore: Int, for workout: HKWorkout) async throws {
	// Un-relate (delete) any existing effort records
	let sampleType = HKQuantityType(.workoutEffortScore)
	let predicate = HKQuery.predicateForWorkoutEffortSamplesRelated(workout: workout, activity: nil)
	let descriptor = HKSampleQueryDescriptor(predicates: [
		HKSamplePredicate.sample(type: sampleType, predicate: predicate)
	], sortDescriptors: [])
	
	for sample in try await descriptor.result(for: healthStore) {
		// May not delete all records: only app-created entries can be deleted
		try? await healthStore.delete(sample)
	}
	
	// Relate new effort
	let effort = HKQuantitySample(
		type: sampleType,
		quantity: HKQuantity(unit: .appleEffortScore(), doubleValue: Double(newScore)),
		start: workout.startDate,
		end: workout.endDate
	)
	
	try await healthStore.relateWorkoutEffortSample(effort, with: workout, activity: nil)
}

A few points to unpack:

Parting Thoughts

I hope you’ve found this article helpful in reading/writing workout effort scores! Developer’s documentations have been an ongoing issue and I hope that Apple can improve that, either through better in-line API documentations or by updating example projects that come with key frameworks.