I am working on a "HealthController" who’s responsibility is to expose helpers for HealthKit and in the future track various health kit data points like heartrate, calories etc…
My setup so far looks like this (This is in a new Swift 6 projects with full concurrency enabled).
import HealthKit
import SwiftUI
@MainActor @Observable
final class HealthController {
// State
let store = HKHealthStore()
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
// Other code for authorization, configuration defaults etc...
func startWatchWorkout(_ name: String, _ startDate: Date) async {
do {
let configuration = HKWorkoutConfiguration()
configuration.activityType = .traditionalStrengthTraining
session = try HKWorkoutSession(healthStore: store, configuration: configuration)
builder = session?.associatedWorkoutBuilder()
builder?.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: configuration)
session?.startActivity(with: startDate)
try await builder?.beginCollection(at: startDate)
} catch {
// ...
}
}
func endWatchWorkout(_ endDate: Date) async {
do {
session?.end()
try await builder?.endCollection(at: endDate)
session = nil
builder = nil
} catch {
// ...
}
}
}
Controller is marked as @MainActor so it’s functions can be called from swift ui views. I am now getting following error at builder?.beginCollection(at: startDate)
and builder?.endCollection(at: endDate)
parts:
Sending main actor-isolated value of type ‘HKLiveWorkoutBuilder’ with
later accesses to nonisolated context risks causing data races
Not sure what the right fix is here, I tried marking builder as nonisolated
, but that seems to cause build error with Observation. I also wrapped each of those calls inside MainActor.run
, but it didn’t get rid of the error.
2
Answers
The problem is that
builder
andsession
are isolated to theMainActor
but notSendable
. You cannot callnonisolated async
methods on it because that would end up sending it off the main actor.You should call
beginCollection
in a non-isolated method, so there are no actor hops that sends non-sendable values. You should only assign the builder and session back to the instance properties (sending them back to the main actor) after you are done with them. Thanks to region-based isolation, Swift understands that this is your last time using the builder and session in the non-isolated method, so they are safe to be sent off.If you send them back to the main actor before that, you are essentially sharing the instance between the main actor and the non-isolated code after that, which of course is not safe with non-sendable values.
I have found another approach for the same issue without using
extension HKLiveWorkoutBuilder: @unchecked @retroactive Sendable {}
.You can declare your builder as a computed property of you class:
Then, you can remove from
startWatchWorkout
method:and remove from
endWatchWorkout
method: