iosswiftwatchoshealthkitswift-concurrency

Concurrency issues with HKLiveWorkoutBuilder managed by @Observable class


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.


Solution

  • 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:

    var builder: HKLiveWorkoutBuilder? {
        session?.associatedWorkoutBuilder() 
    }
    

    Then, you can remove from startWatchWorkout method:

    builder?.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: configuration)
    

    and remove from endWatchWorkout method:

    builder = nil